From 00487547c4aff6bf0c94438768191960a3369365 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Tue, 27 Sep 2022 13:42:55 +0200 Subject: [proxy] facilitate manually pruning orphaned packages (including installed ones) --- src/hydrilla/locales/en_US/LC_MESSAGES/messages.po | 20 +++++- src/hydrilla/proxy/state.py | 15 ++++ .../proxy/state_impl/_operations/prune_orphans.py | 82 +++++++++++++--------- src/hydrilla/proxy/state_impl/base.py | 2 +- src/hydrilla/proxy/state_impl/concrete_state.py | 28 +++++++- src/hydrilla/proxy/state_impl/items.py | 2 +- src/hydrilla/proxy/state_impl/repos.py | 2 +- src/hydrilla/proxy/web_ui/root.py | 10 ++- .../proxy/web_ui/templates/index.html.jinja | 33 +++++++++ 9 files changed, 154 insertions(+), 40 deletions(-) diff --git a/src/hydrilla/locales/en_US/LC_MESSAGES/messages.po b/src/hydrilla/locales/en_US/LC_MESSAGES/messages.po index addfe8e..e33c5e7 100644 --- a/src/hydrilla/locales/en_US/LC_MESSAGES/messages.po +++ b/src/hydrilla/locales/en_US/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: hydrilla 2.0\n" "Report-Msgid-Bugs-To: koszko@koszko.org\n" -"POT-Creation-Date: 2022-09-27 11:23+0200\n" +"POT-Creation-Date: 2022-09-27 13:36+0200\n" "PO-Revision-Date: 2022-02-12 00:00+0000\n" "Last-Translator: Wojtek Kosior \n" "Language: en_US\n" @@ -341,6 +341,24 @@ msgstr "Enable" msgid "web_ui.home.user_make_simple_button" msgstr "Disable" +#: src/hydrilla/proxy/web_ui/templates/index.html.jinja:138 +msgid "web_ui.home.orphans_to_delete_{mappings}" +msgstr "Haketilo is holding some unused packages that can be removed ({mappings})." + +#: src/hydrilla/proxy/web_ui/templates/index.html.jinja:142 +msgid "web_ui.home.orphans_to_delete_exist" +msgstr "Haketilo is holding some unused libraries that can be removed." + +#: src/hydrilla/proxy/web_ui/templates/index.html.jinja:146 +msgid "web_ui.home.orphans_to_delete_{mappings}_{resources}" +msgstr "" +"Haketilo is holding some unused items that can be removed (packages: " +"{mappings}; libraries: {resources})." + +#: src/hydrilla/proxy/web_ui/templates/index.html.jinja:155 +msgid "web_ui.home.prune_orphans_button" +msgstr "Prune orphans" + #: src/hydrilla/proxy/web_ui/templates/items/item_viewversion.html.jinja:25 #: src/hydrilla/proxy/web_ui/templates/prompts/package_suggestion.html.jinja:30 #: src/hydrilla/proxy/web_ui/templates/repos/show_single.html.jinja:33 diff --git a/src/hydrilla/proxy/state.py b/src/hydrilla/proxy/state.py index 7613d54..559a546 100644 --- a/src/hydrilla/proxy/state.py +++ b/src/hydrilla/proxy/state.py @@ -467,6 +467,7 @@ class MappingUseMode(Enum): WHEN_ENABLED = 'W' QUESTION = 'Q' + @dc.dataclass(frozen=True) class HaketiloGlobalSettings: """....""" @@ -481,12 +482,26 @@ class MissingItemError(ValueError): pass +@dc.dataclass(frozen=True) +class OrphanItemsStats: + mappings: int + resources: int + + class HaketiloState(ABC): """....""" @abstractmethod def import_items(self, malcontent_path: Path) -> None: ... + @abstractmethod + def count_orphan_items(self) -> OrphanItemsStats: + ... + + @abstractmethod + def prune_orphan_items(self) -> None: + ... + @abstractmethod def rule_store(self) -> RuleStore: ... diff --git a/src/hydrilla/proxy/state_impl/_operations/prune_orphans.py b/src/hydrilla/proxy/state_impl/_operations/prune_orphans.py index 7123047..5eb8cf7 100644 --- a/src/hydrilla/proxy/state_impl/_operations/prune_orphans.py +++ b/src/hydrilla/proxy/state_impl/_operations/prune_orphans.py @@ -37,37 +37,52 @@ import sqlite3 from pathlib import Path -_remove_item_versions_sqls = [ - ''' - CREATE TEMPORARY TABLE __removed_versions( - item_version_id INTEGER PRIMARY KEY - ); - ''', ''' - INSERT INTO - __removed_versions - SELECT - iv.item_version_id - FROM - item_versions AS iv - JOIN orphan_iterations AS oi USING (repo_iteration_id) - WHERE - iv.installed != 'I'; - ''', ''' - UPDATE - mapping_statuses - SET - active_version_id = NULL - WHERE - active_version_id IN __removed_versions; - ''', ''' - DELETE FROM - item_versions - WHERE - item_version_id IN __removed_versions; - ''', ''' - DROP TABLE __removed_versions; - ''' -] +def _remove_item_versions(cursor: sqlite3.Cursor, with_installed: bool) -> None: + cursor.execute( + ''' + CREATE TEMPORARY TABLE __removed_versions( + item_version_id INTEGER PRIMARY KEY + ); + ''' + ) + + condition = "iv.active != 'R'" if with_installed else "iv.installed != 'I'" + + cursor.execute( + f''' + INSERT INTO + __removed_versions + SELECT + iv.item_version_id + FROM + item_versions AS iv + JOIN orphan_iterations AS oi USING (repo_iteration_id) + WHERE + {condition}; + ''' + ) + + cursor.execute( + ''' + UPDATE + mapping_statuses + SET + active_version_id = NULL + WHERE + active_version_id IN __removed_versions; + ''' + ) + + cursor.execute( + ''' + DELETE FROM + item_versions + WHERE + item_version_id IN __removed_versions; + ''' + ) + + cursor.execute('DROP TABLE __removed_versions;') _remove_items_sql = ''' WITH removed_items AS ( @@ -159,11 +174,10 @@ WHERE repo_id IN removed_repos; ''' -def prune_orphans(cursor: sqlite3.Cursor) -> None: +def prune_orphans(cursor: sqlite3.Cursor, aggressive: bool = False) -> None: assert cursor.connection.in_transaction - for sql in _remove_item_versions_sqls: - cursor.execute(sql) + _remove_item_versions(cursor, with_installed=aggressive) cursor.execute(_remove_items_sql) cursor.execute(_remove_files_sql) cursor.execute(_forget_files_data_sql) diff --git a/src/hydrilla/proxy/state_impl/base.py b/src/hydrilla/proxy/state_impl/base.py index d603504..d99feab 100644 --- a/src/hydrilla/proxy/state_impl/base.py +++ b/src/hydrilla/proxy/state_impl/base.py @@ -227,7 +227,7 @@ class HaketiloStateWithFields(st.HaketiloState): ... @abstractmethod - def prune_orphans(self) -> None: + def soft_prune_orphan_items(self) -> None: ... @abstractmethod diff --git a/src/hydrilla/proxy/state_impl/concrete_state.py b/src/hydrilla/proxy/state_impl/concrete_state.py index 04bb67f..d8706e8 100644 --- a/src/hydrilla/proxy/state_impl/concrete_state.py +++ b/src/hydrilla/proxy/state_impl/concrete_state.py @@ -146,7 +146,33 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): self.rebuild_structures(rules=False) - def prune_orphans(self) -> None: + def count_orphan_items(self) -> st.OrphanItemsStats: + with self.cursor() as cursor: + cursor.execute( + ''' + SELECT + COALESCE(SUM(i.type = 'M'), 0), + COALESCE(SUM(i.type = 'R'), 0) + FROM + item_versions AS iv + JOIN items AS i USING (item_id) + JOIN orphan_iterations AS oi USING (repo_iteration_id) + WHERE + iv.active != 'R'; + ''' + ) + + (orphan_mappings, orphan_resources), = cursor.fetchall() + + return st.OrphanItemsStats(orphan_mappings, orphan_resources) + + def prune_orphan_items(self) -> None: + with self.cursor(transaction=True) as cursor: + _operations.prune_orphans(cursor, aggressive=True) + + self.recompute_dependencies() + + def soft_prune_orphan_items(self) -> None: with self.cursor() as cursor: assert self.connection.in_transaction diff --git a/src/hydrilla/proxy/state_impl/items.py b/src/hydrilla/proxy/state_impl/items.py index e9cabb5..d312db9 100644 --- a/src/hydrilla/proxy/state_impl/items.py +++ b/src/hydrilla/proxy/state_impl/items.py @@ -124,7 +124,7 @@ def _uninstall_version(ref: VersionRefVar) -> t.Optional[VersionRefVar]: _set_installed_status(cursor, ref.id, 'N') - ref.state.prune_orphans() + ref.state.soft_prune_orphan_items() if active_status != 'N': ref.state.recompute_dependencies() diff --git a/src/hydrilla/proxy/state_impl/repos.py b/src/hydrilla/proxy/state_impl/repos.py index 998548e..85117c8 100644 --- a/src/hydrilla/proxy/state_impl/repos.py +++ b/src/hydrilla/proxy/state_impl/repos.py @@ -194,7 +194,7 @@ class ConcreteRepoRef(st.RepoRef): (self.id,) ) - self.state.prune_orphans() + self.state.soft_prune_orphan_items() self.state.recompute_dependencies() def update( diff --git a/src/hydrilla/proxy/web_ui/root.py b/src/hydrilla/proxy/web_ui/root.py index a28fde8..24ff73f 100644 --- a/src/hydrilla/proxy/web_ui/root.py +++ b/src/hydrilla/proxy/web_ui/root.py @@ -111,7 +111,13 @@ app_lock = Lock() @app.route('/', methods=['GET']) def home(errors: t.Mapping[str, bool] = {}) -> werkzeug.Response: - html = flask.render_template('index.html.jinja', **errors) + state = _app.get_haketilo_state() + + html = flask.render_template( + 'index.html.jinja', + orphan_item_stats = state.count_orphan_items(), + **errors + ) return flask.make_response(html, 200) @app.route('/', methods=['POST']) @@ -134,6 +140,8 @@ def home_post() -> werkzeug.Response: state.update_settings(advanced_user=True) elif action == 'user_make_simple': state.update_settings(advanced_user=False) + elif action == 'prune_orphans': + state.prune_orphan_items() else: raise ValueError() diff --git a/src/hydrilla/proxy/web_ui/templates/index.html.jinja b/src/hydrilla/proxy/web_ui/templates/index.html.jinja index d95937a..ce77b24 100644 --- a/src/hydrilla/proxy/web_ui/templates/index.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/index.html.jinja @@ -129,4 +129,37 @@ code in a proprietary work, I am not going to enforce this in court. {'action': 'user_make_simple'}) ]) }} + + {% if orphan_item_stats.mappings > 0 or orphan_item_stats.resources > 0 %} +
+ +

+ {% if settings.advanced_user %} + {% if orphan_item_stats.mappings > 0 %} + {{ + _('web_ui.home.orphans_to_delete_{mappings}') + .format(mappings = orphan_item_stats.mappings) + }} + {% else %} + {{ _('web_ui.home.orphans_to_delete_exist') }} + {% endif %} + {% else %} + {{ + _('web_ui.home.orphans_to_delete_{mappings}_{resources}') + .format( + mappings = orphan_item_stats.mappings, + resources = orphan_item_stats.resources + ) + }} + {% endif %} +

+ + {% set prune_but_text = _('web_ui.home.prune_orphans_button') %} + + {{ + button_row([ + (['green-button'], prune_but_text, {'action': 'prune_orphans'}) + ]) + }} + {% endif %} {% endblock %} -- cgit v1.2.3