From 46a50d7835a3c4c7aefdb7fc3bb52e4e93bf3b8a Mon Sep 17 00:00:00 2001 From: smilerz Date: Fri, 10 Nov 2023 13:33:16 -0600 Subject: [PATCH 1/3] view and delete orphaned files miscelaneous bug fixes discovered during testing --- cookbook/helper/HelperFunctions.py | 2 +- cookbook/helper/recipe_search.py | 2 +- cookbook/templates/system.html | 60 ++++++- cookbook/views/views.py | 56 +++++- recipes/settings.py | 2 +- .../RecipeSearchView/RecipeSearchView.vue | 169 +++++++++--------- 6 files changed, 203 insertions(+), 88 deletions(-) diff --git a/cookbook/helper/HelperFunctions.py b/cookbook/helper/HelperFunctions.py index e2971c2ed2..94f46ee8c9 100644 --- a/cookbook/helper/HelperFunctions.py +++ b/cookbook/helper/HelperFunctions.py @@ -7,7 +7,7 @@ class Round(Func): def str2bool(v): - if type(v) == bool or v is None: + if isinstance(v, bool) or v is None: return v else: return v.lower() in ("yes", "true", "1") diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py index 2b70808e31..e3b0b506fc 100644 --- a/cookbook/helper/recipe_search.py +++ b/cookbook/helper/recipe_search.py @@ -309,7 +309,7 @@ def _recently_viewed(self, num_recent=None): def _favorite_recipes(self, times_cooked=None): if self._sort_includes('favorite') or times_cooked: - less_than = '-' in (times_cooked or []) and not self._sort_includes('-favorite') + less_than = '-' in (str(times_cooked) or []) and not self._sort_includes('-favorite') if less_than: default = 1000 else: diff --git a/cookbook/templates/system.html b/cookbook/templates/system.html index 18e56fa754..90c2eabbf0 100644 --- a/cookbook/templates/system.html +++ b/cookbook/templates/system.html @@ -84,21 +84,47 @@

{% trans 'Debug Mode' %} {% trans 'Database' %} {% if postgres %} + class="badge badge-{% if postgres %}success{% else %}warning{% endif %}">{% if postgres %} {% trans 'Info' %}{% else %}{% trans 'Ok' %}{% endif %}

{% if postgres %} + {% trans 'Everything is fine!' %} + {% else %} {% blocktrans %} This application is not running with a Postgres database backend. This is ok but not recommended as some features only work with postgres databases. {% endblocktrans %} - {% else %} + {% endif %} + +

+ {% trans 'Orphaned Files' %} + + + {% if orphans|length == 0 %}{% trans 'Success' %} + {% elif orphans|length <= 25 %}{% trans 'Warning' %} + {% else %}{% trans 'Danger' %} + {% endif %} + +

+ + {% if orphans|length == 0 %} {% trans 'Everything is fine!' %} + {% else %} + {% blocktrans with orphan_count=orphans|length %} + There are currently {{ orphan_count }} orphaned files. + {% endblocktrans %} +
+ + {% endif %} +

Debug



-{% endblock %} \ No newline at end of file +
+ {% csrf_token %} + +
+{% block script %} + +{% endblock script %} +{% endblock %} + + diff --git a/cookbook/views/views.py b/cookbook/views/views.py index 92c55789e7..f44db8886a 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -3,12 +3,14 @@ from datetime import datetime from uuid import UUID +from django.apps import apps from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.auth.models import Group from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError +from django.db import models from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy @@ -18,6 +20,7 @@ from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, SpaceCreateForm, SpaceJoinForm, User, UserCreateForm, UserPreference) +from cookbook.helper.HelperFunctions import str2bool from cookbook.helper.permission_helper import (group_required, has_group_permission, share_link_valid, switch_user_active_space) from cookbook.models import (Comment, CookLog, InviteLink, SearchFields, SearchPreference, @@ -318,13 +321,20 @@ def system(request): secret_key = False if os.getenv('SECRET_KEY') else True + if request.method == "POST": + del_orphans = request.POST.get('delete_orphans') + orphans = get_orphan_files(delete_orphans=str2bool(del_orphans)) + else: + orphans = get_orphan_files() + return render(request, 'system.html', { 'gunicorn_media': settings.GUNICORN_MEDIA, 'debug': settings.DEBUG, 'postgres': postgres, 'version_info': VERSION_INFO, 'plugins': PLUGINS, - 'secret_key': secret_key + 'secret_key': secret_key, + 'orphans': orphans }) @@ -448,3 +458,47 @@ def test(request): def test2(request): if not settings.DEBUG: return HttpResponseRedirect(reverse('index')) + + +def get_orphan_files(delete_orphans=False): + # Get list of all image files in media folder + media_dir = settings.MEDIA_ROOT + + def find_orphans(): + image_files = [] + for root, dirs, files in os.walk(media_dir): + for file in files: + + if not file.lower().endswith(('.db')) and not root.lower().endswith(('@eadir')): + full_path = os.path.join(root, file) + relative_path = os.path.relpath(full_path, media_dir) + image_files.append((relative_path, full_path)) + + # Get list of all image fields in models + image_fields = [] + for model in apps.get_models(): + for field in model._meta.get_fields(): + if isinstance(field, models.ImageField) or isinstance(field, models.FileField): + image_fields.append((model, field.name)) + + # get all images in the database + # TODO I don't know why, but this completely bypasses scope limitations + image_paths = [] + for model, field in image_fields: + image_field_paths = model.objects.values_list(field, flat=True) + image_paths.extend(image_field_paths) + + # Check each image file against model image fields + return [img for img in image_files if img[0] not in image_paths] + orphans = find_orphans() + if delete_orphans: + for f in [img[1] for img in orphans]: + try: + os.remove(f) + except FileNotFoundError: + print(f"File not found: {f}") + except Exception as e: + print(f"Error deleting file {f}: {e}") + orphans = find_orphans() + + return [img[1] for img in orphans] diff --git a/recipes/settings.py b/recipes/settings.py index 86739628f3..273788eb4a 100644 --- a/recipes/settings.py +++ b/recipes/settings.py @@ -438,7 +438,7 @@ if p['bundle_name'] != '': WEBPACK_LOADER[p['bundle_name']] = { 'CACHE': not DEBUG, - 'BUNDLE_DIR_NAME': f'vue/', # must end with slash + 'BUNDLE_DIR_NAME': 'vue/', # must end with slash 'STATS_FILE': os.path.join(p["base_path"], 'vue', 'webpack-stats.json'), 'POLL_INTERVAL': 0.1, 'TIMEOUT': None, diff --git a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue index 6f0a901b7a..2705b5e3d6 100644 --- a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue +++ b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue @@ -1,6 +1,6 @@ @@ -642,38 +652,38 @@ contain anyall of the following books: {{ k.items.flatMap((x) => x.name).join(", ") }} -
+
- and you can make right now (based on the on hand flag)
+ and you can make right now (based on the on hand flag)
and contain anyall of the following units: {{ search.search_units.flatMap((x) => x.name).join(", ") }}
+ >
and have a rating or equal to {{ search.search_rating }}
+ > or equal to {{ search.search_rating }}
and have been last cooked {{ search.lastcooked }}
+ > {{ search.lastcooked }}
and have been cooked or equal to{{ search.timescooked }} times
+ > or equal to{{ search.timescooked }} times
order by {{ search.sort_order.flatMap((x) => x.text).join(", ") }} -
+
@@ -709,19 +719,19 @@ {{ search.pagination_page }}/{{ Math.ceil(pagination_count / ui.page_size) }} {{ $t("Reset") }} + > {{ search.pagination_page }}/{{ Math.ceil(pagination_count / ui.page_size) }} {{ $t("Reset") }} {{ $t("Random") }} + > {{ $t("Random") }}
@@ -853,24 +863,23 @@