mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-01-02 07:01:23 -05:00
feat: admin maintenance page (#1096)
* fix build typo * generate types * setup maintenance api for common cleanup actions * admin maintenance page * remove duplicate use-with-caution
This commit is contained in:
28
mealie/pkgs/img/static.py
Normal file
28
mealie/pkgs/img/static.py
Normal file
@@ -0,0 +1,28 @@
|
||||
NOT_WEBP = {
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".jpe",
|
||||
".jif",
|
||||
".jfif",
|
||||
".jfi",
|
||||
".png",
|
||||
".gif",
|
||||
".tiff",
|
||||
".tif",
|
||||
".psd",
|
||||
".raw",
|
||||
".arw",
|
||||
".cr2",
|
||||
".nrw",
|
||||
".k25",
|
||||
".bmp",
|
||||
".dib",
|
||||
".heif",
|
||||
".heic",
|
||||
".ind",
|
||||
".jp2",
|
||||
".svg",
|
||||
".svgz",
|
||||
".ai",
|
||||
".eps",
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def pretty_size(size: int) -> str:
|
||||
"""
|
||||
Pretty size takes in a integer value of a file size and returns the most applicable
|
||||
@@ -13,3 +17,17 @@ def pretty_size(size: int) -> str:
|
||||
return f"{round(size / 1024 / 1024 / 1024, 2)} GB"
|
||||
else:
|
||||
return f"{round(size / 1024 / 1024 / 1024 / 1024, 2)} TB"
|
||||
|
||||
|
||||
def get_dir_size(path: Path | str) -> int:
|
||||
"""
|
||||
Get the size of a directory
|
||||
"""
|
||||
total_size = os.path.getsize(path)
|
||||
for item in os.listdir(path):
|
||||
itempath = os.path.join(path, item)
|
||||
if os.path.isfile(itempath):
|
||||
total_size += os.path.getsize(itempath)
|
||||
elif os.path.isdir(itempath):
|
||||
total_size += get_dir_size(itempath)
|
||||
return total_size
|
||||
|
||||
@@ -5,6 +5,7 @@ from . import (
|
||||
admin_backups,
|
||||
admin_email,
|
||||
admin_log,
|
||||
admin_maintenance,
|
||||
admin_management_groups,
|
||||
admin_management_users,
|
||||
admin_server_tasks,
|
||||
@@ -18,4 +19,5 @@ router.include_router(admin_management_users.router)
|
||||
router.include_router(admin_management_groups.router)
|
||||
router.include_router(admin_email.router, tags=["Admin: Email"])
|
||||
router.include_router(admin_server_tasks.router, tags=["Admin: Server Tasks"])
|
||||
router.include_router(admin_backups.router)
|
||||
router.include_router(admin_backups.router, tags=["Admin: Backups"])
|
||||
router.include_router(admin_maintenance.router, tags=["Admin: Maintenance"])
|
||||
|
||||
108
mealie/routes/admin/admin_maintenance.py
Normal file
108
mealie/routes/admin/admin_maintenance.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import contextlib
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from mealie.core.root_logger import LOGGER_FILE
|
||||
from mealie.pkgs.stats import fs_stats
|
||||
from mealie.routes._base import BaseAdminController, controller
|
||||
from mealie.schema.admin import MaintenanceSummary
|
||||
from mealie.schema.response import ErrorResponse, SuccessResponse
|
||||
|
||||
router = APIRouter(prefix="/maintenance")
|
||||
|
||||
|
||||
def clean_images(root_dir: Path, dry_run: bool) -> int:
|
||||
cleaned_images = 0
|
||||
|
||||
for recipe_dir in root_dir.iterdir():
|
||||
image_dir = recipe_dir.joinpath("images")
|
||||
|
||||
if not image_dir.exists():
|
||||
continue
|
||||
|
||||
for image in image_dir.iterdir():
|
||||
if image.is_dir():
|
||||
continue
|
||||
|
||||
if image.suffix != ".webp":
|
||||
if not dry_run:
|
||||
image.unlink()
|
||||
|
||||
cleaned_images += 1
|
||||
|
||||
return cleaned_images
|
||||
|
||||
|
||||
def clean_recipe_folders(root_dir: Path, dry_run: bool) -> int:
|
||||
cleaned_dirs = 0
|
||||
|
||||
for recipe_dir in root_dir.iterdir():
|
||||
if recipe_dir.is_dir():
|
||||
# Attemp to convert the folder name to a UUID
|
||||
try:
|
||||
uuid.UUID(recipe_dir.name)
|
||||
continue
|
||||
except ValueError:
|
||||
if not dry_run:
|
||||
shutil.rmtree(recipe_dir)
|
||||
cleaned_dirs += 1
|
||||
|
||||
return cleaned_dirs
|
||||
|
||||
|
||||
@controller(router)
|
||||
class AdminMaintenanceController(BaseAdminController):
|
||||
@router.get("", response_model=MaintenanceSummary)
|
||||
def get_maintenance_summary(self):
|
||||
"""
|
||||
Get the maintenance summary
|
||||
"""
|
||||
log_file_size = 0
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
log_file_size = os.path.getsize(LOGGER_FILE)
|
||||
|
||||
return MaintenanceSummary(
|
||||
data_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.deps.folders.DATA_DIR)),
|
||||
log_file_size=fs_stats.pretty_size(log_file_size),
|
||||
cleanable_images=clean_images(self.deps.folders.RECIPE_DATA_DIR, dry_run=True),
|
||||
cleanable_dirs=clean_recipe_folders(self.deps.folders.RECIPE_DATA_DIR, dry_run=True),
|
||||
)
|
||||
|
||||
@router.post("/clean/images", response_model=SuccessResponse)
|
||||
def clean_images(self):
|
||||
"""
|
||||
Purges all the images from the filesystem that aren't .webp
|
||||
"""
|
||||
try:
|
||||
cleaned_images = clean_images(self.deps.folders.RECIPE_DATA_DIR, dry_run=False)
|
||||
return SuccessResponse.respond(f"{cleaned_images} Images cleaned")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=ErrorResponse.respond("Failed to clean images")) from e
|
||||
|
||||
@router.post("/clean/recipe-folders", response_model=SuccessResponse)
|
||||
def clean_recipe_folders(self):
|
||||
"""
|
||||
Deletes all the recipe folders that don't have names that are valid UUIDs
|
||||
"""
|
||||
try:
|
||||
cleaned_dirs = clean_recipe_folders(self.deps.folders.RECIPE_DATA_DIR, dry_run=False)
|
||||
return SuccessResponse.respond(f"{cleaned_dirs} Recipe folders removed")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=ErrorResponse.respond("Failed to clean directories")) from e
|
||||
|
||||
@router.post("/clean/logs", response_model=SuccessResponse)
|
||||
def clean_logs(self):
|
||||
"""
|
||||
Purges the logs
|
||||
"""
|
||||
try:
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
os.remove(LOGGER_FILE)
|
||||
LOGGER_FILE.touch()
|
||||
return SuccessResponse.respond("Logs cleaned")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=ErrorResponse.respond("Failed to clean logs")) from e
|
||||
@@ -1,6 +1,7 @@
|
||||
# GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
from .about import *
|
||||
from .backup import *
|
||||
from .maintenance import *
|
||||
from .migration import *
|
||||
from .restore import *
|
||||
from .settings import *
|
||||
|
||||
8
mealie/schema/admin/maintenance.py
Normal file
8
mealie/schema/admin/maintenance.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from fastapi_camelcase import CamelModel
|
||||
|
||||
|
||||
class MaintenanceSummary(CamelModel):
|
||||
data_dir_size: str
|
||||
log_file_size: str
|
||||
cleanable_images: int
|
||||
cleanable_dirs: int
|
||||
Reference in New Issue
Block a user