diff options
17 files changed, 365 insertions, 209 deletions
diff --git a/src/hydrilla/proxy/state.py b/src/hydrilla/proxy/state.py index 0c6dff3..0e45725 100644 --- a/src/hydrilla/proxy/state.py +++ b/src/hydrilla/proxy/state.py @@ -47,6 +47,7 @@ from ..exceptions import HaketiloException from ..versions import VerTuple from ..url_patterns import ParsedPattern from .. import item_infos +from .simple_dependency_satisfying import ImpossibleSituation class EnabledStatus(Enum): @@ -112,11 +113,19 @@ class RepoUrlInvalid(HaketiloException): class RepoCommunicationError(HaketiloException): pass -@dc.dataclass -class FileInstallationError(RepoCommunicationError): +@dc.dataclass(frozen=True) +class FileInstallationError(HaketiloException): repo_id: str sha256: str +@dc.dataclass(frozen=True) +class FileIntegrityError(FileInstallationError): + invalid_sha256: str + +@dc.dataclass(frozen=True) +class FileMissingError(FileInstallationError): + pass + class RepoApiVersionUnsupported(HaketiloException): pass @@ -214,8 +223,11 @@ class MappingStore(Store[MappingRef]): class MappingVersionRef(Ref): """....""" @abstractmethod - def update_status(self, new_status: EnabledStatus) -> None: - """....""" + def install(self) -> None: + ... + + @abstractmethod + def uninstall(self) -> None: ... @abstractmethod diff --git a/src/hydrilla/proxy/state_impl/_operations/__init__.py b/src/hydrilla/proxy/state_impl/_operations/__init__.py index c147be4..ff34b0b 100644 --- a/src/hydrilla/proxy/state_impl/_operations/__init__.py +++ b/src/hydrilla/proxy/state_impl/_operations/__init__.py @@ -4,6 +4,7 @@ # # Available under the terms of Creative Commons Zero v1.0 Universal. -from .load_packages import load_packages, FileResolver from .prune_packages import prune_packages +from .pull_missing_files import pull_missing_files +from .load_packages import _load_packages_no_state_update from .recompute_dependencies import _recompute_dependencies_no_state_update diff --git a/src/hydrilla/proxy/state_impl/_operations/load_packages.py b/src/hydrilla/proxy/state_impl/_operations/load_packages.py index 78e8024..16d1154 100644 --- a/src/hydrilla/proxy/state_impl/_operations/load_packages.py +++ b/src/hydrilla/proxy/state_impl/_operations/load_packages.py @@ -32,20 +32,19 @@ from __future__ import annotations import io -import hashlib +import mimetypes +import sqlite3 import dataclasses as dc import typing as t -from pathlib import Path -from abc import ABC, abstractmethod - -import sqlite3 +from pathlib import Path, PurePosixPath -from ....exceptions import HaketiloException -from ....translations import smart_gettext as _ from .... import versions from .... import item_infos - +from ... import state +from .recompute_dependencies import _recompute_dependencies_no_state_update, \ + FileResolver +from .prune_packages import prune_packages def make_repo_iteration(cursor: sqlite3.Cursor, repo_id: int) -> int: cursor.execute( @@ -128,8 +127,9 @@ def get_or_make_item(cursor: sqlite3.Cursor, type: str, identifier: str) -> int: def make_item_version( cursor: sqlite3.Cursor, item_id: int, - repo_iteration_id: int, version: versions.VerTuple, + installed: str, + repo_iteration_id: int, definition: bytes ) -> int: ver_str = versions.version_string(version) @@ -143,9 +143,9 @@ def make_item_version( repo_iteration_id, definition ) - VALUES(?, ?, 'I', ?, ?); + VALUES(?, ?, ?, ?, ?); ''', - (item_id, ver_str, repo_iteration_id, definition) + (item_id, ver_str, installed, repo_iteration_id, definition) ) cursor.execute( @@ -173,27 +173,10 @@ def make_mapping_status(cursor: sqlite3.Cursor, item_id: int) -> None: (item_id,) ) -def get_or_make_file(cursor: sqlite3.Cursor, sha256: str, file_bytes: bytes) \ - -> int: - cursor.execute( - ''' - INSERT OR IGNORE INTO files(sha256, data) - VALUES(?, ?) - ''', - (sha256, file_bytes) - ) +def get_or_make_file(cursor: sqlite3.Cursor, sha256: str) -> int: + cursor.execute('INSERT OR IGNORE INTO files(sha256) VALUES(?);', (sha256,)) - cursor.execute( - ''' - SELECT - file_id - FROM - files - WHERE - sha256 = ?; - ''', - (sha256,) - ) + cursor.execute('SELECT file_id FROM files WHERE sha256 = ?;', (sha256,)) (file_id,), = cursor.fetchall() @@ -225,20 +208,15 @@ def make_file_use( @dc.dataclass(frozen=True) class _FileInfo: - id: int - is_ascii: bool - -class FileResolver(ABC): - @abstractmethod - def by_sha256(self, sha256: str) -> bytes: - ... + id: int + extension: str def _add_item( cursor: sqlite3.Cursor, - package_file_resolver: FileResolver, info: item_infos.AnyInfo, definition: bytes, - repo_iteration_id: int + repo_iteration_id: int, + repo_id: int ) -> None: item_id = get_or_make_item(cursor, info.type_name, info.identifier) @@ -246,11 +224,12 @@ def _add_item( make_mapping_status(cursor, item_id) item_version_id = make_item_version( - cursor, - item_id, - repo_iteration_id, - info.version, - definition + cursor = cursor, + item_id = item_id, + version = info.version, + installed = 'I' if repo_id == 1 else 'N', + repo_iteration_id = repo_iteration_id, + definition = definition ) file_infos = {} @@ -260,29 +239,24 @@ def _add_item( file_specifiers.extend(info.scripts) for file_spec in file_specifiers: - file_bytes = package_file_resolver.by_sha256(file_spec.sha256) - - sha256 = hashlib.sha256(file_bytes).digest().hex() - if sha256 != file_spec.sha256: - fmt = _('err.proxy.file_hash_mismatched_{item_identifier}_{file_name}_{expected_sha256}_{actual_sha256}') - msg = fmt.format( - item_identifier = info.identifier, - file_name = file_spec.name, - expected_sha256 = file_spec.sha256, - actual_sha256 = sha256 - ) - raise HaketiloException(msg) + file_id = get_or_make_file(cursor, file_spec.sha256) - file_id = get_or_make_file(cursor, sha256, file_bytes) + suffix = PurePosixPath(file_spec.name).suffix - file_infos[sha256] = _FileInfo(file_id, file_bytes.isascii()) + file_infos[file_spec.sha256] = _FileInfo(file_id, suffix) for idx, file_spec in enumerate(info.source_copyright): file_info = file_infos[file_spec.sha256] - if file_info.is_ascii: - mime = 'text/plain' - else: + + mime = mimetypes.types_map.get(file_info.extension) + if mime is None: + mime = mimetypes.common_types.get(file_info.extension) + if mime is None: mime = 'application/octet-stream' + if mime is None and file_info.extension == '.spdx': + # We don't know of any estabilished mime type for tag-value SPDX + # reports. Let's use the following for now. + mime = 'text/spdx' make_file_use( cursor, @@ -342,22 +316,17 @@ class MalcontentFileResolver(FileResolver): def by_sha256(self, sha256: str) -> bytes: file_path = self.malcontent_dir_path / 'file' / 'sha256' / sha256 if not file_path.is_file(): - fmt = _('err.proxy.file_missing_{sha256}') - raise HaketiloException(fmt.format(sha256=sha256)) + raise state.FileMissingError(repo_id='1', sha256=sha256) return file_path.read_bytes() -def load_packages( +def _load_packages_no_state_update( cursor: sqlite3.Cursor, malcontent_path: Path, - repo_id: int, - package_file_resolver: t.Optional[FileResolver] = None + repo_id: int ) -> int: assert cursor.connection.in_transaction - if package_file_resolver is None: - package_file_resolver = MalcontentFileResolver(malcontent_path) - repo_iteration_id = make_repo_iteration(cursor, repo_id) types: t.Iterable[t.Type[item_infos.AnyInfo]] = \ @@ -371,11 +340,16 @@ def load_packages( info_type ): _add_item( - cursor, - package_file_resolver, - info, - definition, - repo_iteration_id + cursor = cursor, + info = info, + definition = definition, + repo_iteration_id = repo_iteration_id, + repo_id = repo_id ) + _recompute_dependencies_no_state_update( + cursor = cursor, + semirepo_file_resolver = MalcontentFileResolver(malcontent_path) + ) + return repo_iteration_id diff --git a/src/hydrilla/proxy/state_impl/_operations/prune_packages.py b/src/hydrilla/proxy/state_impl/_operations/prune_packages.py index eb0539c..6f4b3e7 100644 --- a/src/hydrilla/proxy/state_impl/_operations/prune_packages.py +++ b/src/hydrilla/proxy/state_impl/_operations/prune_packages.py @@ -137,28 +137,8 @@ WHERE def prune_packages(cursor: sqlite3.Cursor) -> None: assert cursor.connection.in_transaction - print('VERSIONS TO DELETE', cursor.execute(''' -SELECT - iv.item_version_id - FROM - item_versions AS iv - JOIN orphan_iterations AS oi USING (repo_iteration_id) - WHERE - iv.installed != 'I'; -''').fetchall()) for sql in _remove_item_versions_sqls: cursor.execute(sql) - print('ITEMS TO DELETE', cursor.execute(''' -SELECT - i.item_id - FROM - items AS i - LEFT JOIN item_versions AS iv USING (item_id) - LEFT JOIN mapping_statuses AS ms USING (item_id) - WHERE - iv.item_version_id IS NULL AND - (i.type = 'R' OR ms.enabled = 'N'); -''').fetchall()) cursor.execute(_remove_items_sql) cursor.execute(_remove_files_sql) cursor.execute(_remove_repo_iterations_sql) diff --git a/src/hydrilla/proxy/state_impl/_operations/pull_missing_files.py b/src/hydrilla/proxy/state_impl/_operations/pull_missing_files.py new file mode 100644 index 0000000..04a2910 --- /dev/null +++ b/src/hydrilla/proxy/state_impl/_operations/pull_missing_files.py @@ -0,0 +1,113 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +# Haketilo proxy data and configuration (download of package files). +# +# 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 sqlite3 +import hashlib + +from abc import ABC, abstractmethod +from pathlib import Path +from urllib.parse import urljoin + +import requests + +from ... import state + + +class FileResolver(ABC): + @abstractmethod + def by_sha256(self, sha256: str) -> bytes: + ... + +class DummyFileResolver(FileResolver): + def by_sha256(self, sha256: str) -> bytes: + raise NotImplementedError() + +def pull_missing_files( + cursor: sqlite3.Cursor, + semirepo_file_resolver: FileResolver = DummyFileResolver() +) -> None: + cursor.execute( + ''' + SELECT DISTINCT + f.file_id, f.sha256, + r.repo_id, r.url + FROM + repos AS R + JOIN repo_iterations AS ri USING (repo_id) + JOIN item_versions AS iv USING (repo_iteration_id) + JOIN file_uses AS fu USING (item_version_id) + JOIN files AS f USING (file_id) + WHERE + iv.installed = 'I' AND f.data IS NULL; + ''' + ) + + rows = cursor.fetchall() + + for file_id, sha256, repo_id, repo_url in rows: + if repo_id == 1: + file_bytes = semirepo_file_resolver.by_sha256(sha256) + else: + try: + url = urljoin(repo_url, f'file/sha256/{sha256}') + response = requests.get(url) + except: + raise state.RepoCommunicationError() + + if not response.ok: + raise state.FileMissingError( + repo_id = str(repo_id), + sha256 = sha256 + ) + + file_bytes = response.content + + computed_sha256 = hashlib.sha256(file_bytes).digest().hex() + if computed_sha256 != sha256: + raise state.FileIntegrityError( + repo_id = str(repo_id), + sha256 = sha256, + invalid_sha256 = computed_sha256 + ) + + cursor.execute( + ''' + UPDATE + files + SET + data = ? + WHERE + file_id = ?; + ''', + (file_bytes, file_id) + ) diff --git a/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py b/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py index 2b18a51..494d130 100644 --- a/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py +++ b/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py @@ -34,13 +34,10 @@ from __future__ import annotations import sqlite3 import typing as t -from urllib.parse import urlparse, urljoin - -import requests - from .... import item_infos from ... import simple_dependency_satisfying as sds -from ... import state +from .pull_missing_files import pull_missing_files, FileResolver, \ + DummyFileResolver AnyInfoVar = t.TypeVar( @@ -165,7 +162,8 @@ def _recompute_dependencies_no_state_update_no_pull_files( for choice in mapping_choices.values(): mapping_ver_id = mappings_to_ids[choice.info.identifier] - _mark_version_installed(cursor, mapping_ver_id) + if choice.required: + _mark_version_installed(cursor, mapping_ver_id) cursor.execute( ''' @@ -200,7 +198,7 @@ def _recompute_dependencies_no_state_update_no_pull_files( INSERT INTO payloads( mapping_item_id, pattern, - eval_allowed, + eval_allowed, cors_bypass_allowed ) VALUES (?, ?, ?, ?); @@ -230,7 +228,8 @@ def _recompute_dependencies_no_state_update_no_pull_files( for res_num, resource_info in enumerate(payload.resources): resource_ver_id = resources_to_ids[resource_info.identifier] - _mark_version_installed(cursor, resource_ver_id) + if choice.required: + _mark_version_installed(cursor, resource_ver_id) cursor.execute( ''' @@ -244,53 +243,14 @@ def _recompute_dependencies_no_state_update_no_pull_files( (payload_id, resource_ver_id, res_num) ) -def _pull_missing_files(cursor: sqlite3.Cursor) -> None: - cursor.execute( - ''' - SELECT DISTINCT - f.file_id, f.sha256, - r.repo_id, r.url - FROM - repos AS R - JOIN repo_iterations AS ri USING (repo_id) - JOIN item_versions AS iv USING (repo_iteration_id) - JOIN file_uses AS fu USING (item_version_id) - JOIN files AS f USING (file_id) - WHERE - iv.installed = 'I' AND f.data IS NULL; - ''' - ) - - rows = cursor.fetchall() - - for file_id, sha256, repo_id, repo_url in rows: - try: - response = requests.get(urljoin(repo_url, f'file/sha256/{sha256}')) - assert response.ok - except: - raise state.FileInstallationError( - repo_id = str(repo_id), - sha256 = sha256 - ) - - cursor.execute( - ''' - UPDATE - files - SET - data = ? - WHERE - file_id = ?; - ''', - (response.content, file_id) - ) - def _recompute_dependencies_no_state_update( - cursor: sqlite3.Cursor, - extra_requirements: t.Iterable[sds.MappingRequirement] + cursor: sqlite3.Cursor, + extra_requirements: t.Iterable[sds.MappingRequirement] = (), + semirepo_file_resolver: FileResolver = DummyFileResolver() ) -> None: _recompute_dependencies_no_state_update_no_pull_files( cursor, extra_requirements ) - _pull_missing_files(cursor) + + pull_missing_files(cursor, semirepo_file_resolver) diff --git a/src/hydrilla/proxy/state_impl/concrete_state.py b/src/hydrilla/proxy/state_impl/concrete_state.py index ec16e11..ef698a0 100644 --- a/src/hydrilla/proxy/state_impl/concrete_state.py +++ b/src/hydrilla/proxy/state_impl/concrete_state.py @@ -122,9 +122,13 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): def import_packages(self, malcontent_path: Path) -> None: with self.cursor(transaction=True) as cursor: - _operations.load_packages(cursor, malcontent_path, 1) + _operations._load_packages_no_state_update( + cursor = cursor, + malcontent_path = malcontent_path, + repo_id = 1 + ) - self.recompute_dependencies() + self.rebuild_structures() def recompute_dependencies( self, @@ -134,8 +138,8 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): assert self.connection.in_transaction _operations._recompute_dependencies_no_state_update( - cursor, - extra_requirements + cursor = cursor, + extra_requirements = extra_requirements ) self.rebuild_structures() diff --git a/src/hydrilla/proxy/state_impl/mappings.py b/src/hydrilla/proxy/state_impl/mappings.py index e5b324d..946df14 100644 --- a/src/hydrilla/proxy/state_impl/mappings.py +++ b/src/hydrilla/proxy/state_impl/mappings.py @@ -31,13 +31,14 @@ This module provides an interface to interact with mappings inside Haketilo. # Enable using with Python 3.7. from __future__ import annotations -import io +import sqlite3 import typing as t import dataclasses as dc from ... import item_infos from .. import state as st from . import base +from . import _operations @dc.dataclass(frozen=True, unsafe_hash=True) @@ -112,19 +113,13 @@ class ConcreteMappingStore(st.MappingStore): SELECT DISTINCT item_id, identifier, - CASE WHEN enabled IN ('A', 'E') THEN item_version_id - ELSE NULL END, - CASE WHEN enabled IN ('A', 'E') THEN definition - ELSE NULL END, - CASE WHEN enabled IN ('A', 'E') THEN repo - ELSE NULL END, - CASE WHEN enabled IN ('A', 'E') THEN repo_iteration - ELSE NULL END, + CASE WHEN is_active THEN item_version_id ELSE NULL END, + CASE WHEN is_active THEN definition ELSE NULL END, + CASE WHEN is_active THEN repo ELSE NULL END, + CASE WHEN is_active THEN repo_iteration ELSE NULL END, enabled FROM - mapping_display_infos - WHERE - is_active OR item_version_id IS NULL; + mapping_display_infos; ''' ) @@ -136,8 +131,8 @@ class ConcreteMappingStore(st.MappingStore): repo_iteration, status_letter) in rows: ref = ConcreteMappingRef(str(item_id), self.state) - version_ref: t.Optional[st.MappingVersionRef] = None - item_info: t.Optional[item_infos.MappingInfo] = None + active_version_ref: t.Optional[st.MappingVersionRef] = None + item_info: t.Optional[item_infos.MappingInfo] = None if item_version_id is not None: active_version_ref = ConcreteMappingVersionRef( @@ -162,14 +157,52 @@ class ConcreteMappingStore(st.MappingStore): return result + @dc.dataclass(frozen=True, unsafe_hash=True) class ConcreteMappingVersionRef(st.MappingVersionRef): state: base.HaketiloStateWithFields = dc.field(hash=False, compare=False) - def update_status(self, new_status: st.EnabledStatus) -> None: - """....""" - assert new_status != st.EnabledStatus.AUTO_ENABLED + def _set_installed_status( + self, + cursor: sqlite3.Cursor, + new_status: st.InstalledStatus + ) -> None: + cursor.execute( + ''' + UPDATE + item_versions + SET + installed = ? + WHERE + item_version_id = ?; + ''', + (new_status.value, self.id,) + ) + + def install(self) -> None: + with self.state.cursor(transaction=True) as cursor: + info = self.get_display_info() + + if info.installed == st.InstalledStatus.INSTALLED: + return + + self._set_installed_status(cursor, st.InstalledStatus.INSTALLED) + + _operations.pull_missing_files(cursor) + + def uninstall(self) -> None: raise NotImplementedError() + # with self.state.cursor(transaction=True) as cursor: + # info = self.get_display_info() + + # if info.installed == st.InstalledStatus.NOT_INSTALLED: + # return + + # if info.installed == st.InstalledStatus.FAILED_TO_INSTALL: + # self._set_installed_status(st.InstalledStatus.UNINSTALLED) + # return + # + # .... def get_display_info(self) -> st.MappingVersionDisplayInfo: with self.state.cursor() as cursor: diff --git a/src/hydrilla/proxy/state_impl/repos.py b/src/hydrilla/proxy/state_impl/repos.py index 2670ae9..d4fbbce 100644 --- a/src/hydrilla/proxy/state_impl/repos.py +++ b/src/hydrilla/proxy/state_impl/repos.py @@ -145,16 +145,6 @@ def sync_remote_repo_definitions(repo_url: str, dest: Path) -> None: item_path.write_bytes(item_response.content) -@dc.dataclass(frozen=True) -class RemoteFileResolver(_operations.FileResolver): - repo_url: str - - def by_sha256(self, sha256: str) -> bytes: - response = requests.get(urljoin(self.repo_url, f'file/sha256/{sha256}')) - assert response.ok - return response.content - - def make_repo_display_info( ref: st.RepoRef, name: str, @@ -290,11 +280,10 @@ class ConcreteRepoRef(st.RepoRef): with tempfile.TemporaryDirectory() as tmpdir_str: tmpdir = Path(tmpdir_str) sync_remote_repo_definitions(repo_url, tmpdir) - new_iteration_id = _operations.load_packages( - cursor, - tmpdir, - int(self.id), - RemoteFileResolver(repo_url) + new_iteration_id = _operations._load_packages_no_state_update( + cursor = cursor, + malcontent_path = tmpdir, + repo_id = int(self.id) ) self.state.rebuild_structures() diff --git a/src/hydrilla/proxy/tables.sql b/src/hydrilla/proxy/tables.sql index 3b84741..aa98081 100644 --- a/src/hydrilla/proxy/tables.sql +++ b/src/hydrilla/proxy/tables.sql @@ -241,7 +241,7 @@ SELECT i.item_id, i.identifier, CASE WHEN ms.enabled = 'N' AND - (ms.required OR COUNT(p.payload_id) > 0) + COALESCE(ms.active_version_id, -1) = iv.item_version_id THEN 'A' -- AUTO_ENABLED mapping ELSE @@ -260,18 +260,10 @@ FROM USING (item_id) JOIN item_versions AS iv USING (item_id) - LEFT JOIN payloads AS p - ON iv.item_version_id = p.mapping_item_id JOIN repo_iterations AS ri USING (repo_iteration_id) JOIN repos AS r - USING (repo_id) -WHERE - i.type = 'M' -GROUP BY - ms.enabled, ms.required, ms.active_version_id, - iv.item_version_id, iv.definition, iv.installed, - r.repo_id, r.active_iteration_id, r.name, ri.iteration; + USING (repo_id); CREATE TABLE resolved_depended_resources( payload_id INTEGER, diff --git a/src/hydrilla/proxy/web_ui/packages.py b/src/hydrilla/proxy/web_ui/packages.py index 85e5d00..ac8b480 100644 --- a/src/hydrilla/proxy/web_ui/packages.py +++ b/src/hydrilla/proxy/web_ui/packages.py @@ -132,15 +132,50 @@ def show_package(mapping_id: str) -> werkzeug.Response: flask.abort(404) @bp.route('/packages/viewversion/<string:mapping_version_id>') -def show_package_version(mapping_version_id: str) -> werkzeug.Response: +def show_package_version( + mapping_version_id: str, + errors: t.Mapping[str, bool] = {} +) -> werkzeug.Response: try: store = _app.get_haketilo_state().mapping_version_store() display_info = store.get(mapping_version_id).get_display_info() html = flask.render_template( 'packages/show_single_version.html.jinja', - display_info = display_info + display_info = display_info, + **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.uninstall() + return flask.redirect(flask.url_for('.packages')) + else: + raise ValueError() + 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/repos.py b/src/hydrilla/proxy/web_ui/repos.py index ae4741f..5aa5a21 100644 --- a/src/hydrilla/proxy/web_ui/repos.py +++ b/src/hydrilla/proxy/web_ui/repos.py @@ -129,6 +129,10 @@ def alter_repo(repo_id: str) -> werkzeug.Response: return show_repo(repo_id, {'repo_url_invalid': True}) except st.RepoCommunicationError: return show_repo(repo_id, {'repo_communication_error': True}) + except st.FileInstallationError: + # We'll add the ability to present more meaningful errors later. For now + # let's treat file errors the same as repo communication errors. + return show_repo(repo_id, {'repo_communication_error': True}) except st.RepoApiVersionUnsupported: return show_repo(repo_id, {'repo_api_version_unsupported': True}) except st.MissingItemError: diff --git a/src/hydrilla/proxy/web_ui/root.py b/src/hydrilla/proxy/web_ui/root.py index b2c1a43..c3e0c80 100644 --- a/src/hydrilla/proxy/web_ui/root.py +++ b/src/hydrilla/proxy/web_ui/root.py @@ -82,6 +82,8 @@ class WebUIAppImpl(_app.WebUIApp): self.jinja_env.globals['versions'] = versions self.jinja_env.globals['get_current_endpoint'] = get_current_endpoint + self.jinja_env.globals['InstalledStatus'] = st.InstalledStatus + self.jinja_env.globals['EnabledStatus'] = st.EnabledStatus self.before_request(authenticate_by_referrer) diff --git a/src/hydrilla/proxy/web_ui/templates/base.html.jinja b/src/hydrilla/proxy/web_ui/templates/base.html.jinja index b41a106..a34d538 100644 --- a/src/hydrilla/proxy/web_ui/templates/base.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/base.html.jinja @@ -45,6 +45,12 @@ in a proprietary work, I am not going to enforce this in court. color: inherit; } + .inline-form { + display: inline-block; + padding: 0; + margin: 0; + } + .small-print { font-size: 80%; color: #555; 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 beedd43..1b46da7 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 @@ -25,6 +25,16 @@ in a proprietary work, I am not going to enforce this in court. {{ super() }} {% endblock %} {% block main %} + {%- 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.packages.single_version.heading.name_{}') @@ -35,4 +45,45 @@ in a proprietary work, I am not going to enforce this in court. {{ display_info.info.versioned_identifier }} </div> TODO: add more info... -{% endblock %} + {%- if display_info.installed == InstalledStatus.INSTALLED %} + <div> + {{ _('web_ui.packages.single_version.package_is_installed') }} + </div> + {%- if uninstall_disallowed is not defined %} + <form method="POST"> + <input name="action" value="uninstall_package" type="hidden"> + <button class="green-button"> + {{ _('web_ui.packages.single_version.uninstall_button') }} + </button> + </form> + {%- endif %} + {%- elif display_info.installed == InstalledStatus.NOT_INSTALLED %} + <div> + {{ _('web_ui.packages.single_version.package_is_not_installed') }} + </div> + <form method="POST"> + <input name="action" value="install_package" type="hidden"> + <button class="green-button"> + {{ _('web_ui.packages.single_version.install_button') }} + </button> + </form> + {%- else %} + <div> + {{ _('web_ui.packages.single_version.package_install_failed') }} + </div> + <div> + <form method="POST" class="inline-form"> + <input name="action" value="install_package" 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"> + <button class="green-button"> + {{ _('web_ui.packages.single_version.leave_uninstalled_button') }} + </button> + </form> + </div> + {%- endif %} +{%- endblock %} diff --git a/src/hydrilla/proxy/web_ui/templates/repos/add.html.jinja b/src/hydrilla/proxy/web_ui/templates/repos/add.html.jinja index 3fdcbd3..1b618cf 100644 --- a/src/hydrilla/proxy/web_ui/templates/repos/add.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/repos/add.html.jinja @@ -31,12 +31,12 @@ in a proprietary work, I am not going to enforce this in court. </div> {% if repo_name_invalid is defined -%} <aside class="error-note"> - {{ _('web_ui.repos.add.repo_name_invalid') }} + {{ _('web_ui.err.repo_name_invalid') }} </aside> {%- endif %} {% if repo_name_taken is defined -%} <aside class="error-note"> - {{ _('web_ui.repos.add.repo_name_taken') }} + {{ _('web_ui.err.repo_name_taken') }} </aside> {%- endif %} <div> @@ -49,7 +49,7 @@ in a proprietary work, I am not going to enforce this in court. </div> {% if repo_url_invalid is defined -%} <aside class="error-note"> - {{ _('web_ui.repos.add.repo_url_invalid') }} + {{ _('web_ui.err.repo_url_invalid') }} </aside> {%- endif %} <div> diff --git a/src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja b/src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja index b3a4f32..a5998b8 100644 --- a/src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja @@ -29,12 +29,12 @@ in a proprietary work, I am not going to enforce this in court. {% block main %} {%- if repo_communication_error is defined %} <aside class="error-note"> - {{ _('web_ui.repos.add.repo_communication_error') }} + {{ _('web_ui.err.repo_communication_error') }} </aside> {%- endif %} {%- if repo_api_version_unsupported is defined %} <aside class="error-note"> - {{ _('web_ui.repos.add.repo_api_version_unsupported') }} + {{ _('web_ui.err.repo_api_version_unsupported') }} </aside> {%- endif %} {%- set repo_id = display_info.ref.id %} @@ -61,12 +61,12 @@ in a proprietary work, I am not going to enforce this in court. <input type="hidden" name="action" value="update_repo_data"> {%- if repo_name_invalid is defined %} <aside class="error-note"> - {{ _('web_ui.repos.add.repo_name_invalid') }} + {{ _('web_ui.err.repo_name_invalid') }} </aside> {%- endif %} {%- if repo_name_taken is defined %} <aside class="error-note"> - {{ _('web_ui.repos.add.repo_name_taken') }} + {{ _('web_ui.err.repo_name_taken') }} </aside> {%- endif %} <div> @@ -109,7 +109,7 @@ in a proprietary work, I am not going to enforce this in court. <input type="hidden" name="action" value="update_repo_data"> {%- if repo_url_invalid is defined %} <aside class="error-note"> - {{ _('web_ui.repos.add.repo_url_invalid') }} + {{ _('web_ui.err.repo_url_invalid') }} </aside> {%- endif %} <div> |