mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04: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:
		
							
								
								
									
										2
									
								
								.github/workflows/backend-docker-nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/backend-docker-nightly.yml
									
									
									
									
										vendored
									
									
								
							| @@ -47,7 +47,7 @@ jobs: | |||||||
|           docker build --push --no-cache \ |           docker build --push --no-cache \ | ||||||
|             --tag hkotel/mealie:api-nightly \ |             --tag hkotel/mealie:api-nightly \ | ||||||
|             --platform linux/amd64,linux/arm64 . |             --platform linux/amd64,linux/arm64 . | ||||||
|             --build-arg COMMIT=$(git rev-parse HEAD) \ |             --build-args COMMIT=$(git rev-parse HEAD) \ | ||||||
|       # |       # | ||||||
|       # Build Discord Notification |       # Build Discord Notification | ||||||
|       # |       # | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import { AdminTaskAPI } from "./admin/admin-tasks"; | |||||||
| import { AdminUsersApi } from "./admin/admin-users"; | import { AdminUsersApi } from "./admin/admin-users"; | ||||||
| import { AdminGroupsApi } from "./admin/admin-groups"; | import { AdminGroupsApi } from "./admin/admin-groups"; | ||||||
| import { AdminBackupsApi } from "./admin/admin-backups"; | import { AdminBackupsApi } from "./admin/admin-backups"; | ||||||
|  | import { AdminMaintenanceApi } from "./admin/admin-maintenance"; | ||||||
| import { ApiRequestInstance } from "~/types/api"; | import { ApiRequestInstance } from "~/types/api"; | ||||||
|  |  | ||||||
| export class AdminAPI { | export class AdminAPI { | ||||||
| @@ -11,6 +12,7 @@ export class AdminAPI { | |||||||
|   public users: AdminUsersApi; |   public users: AdminUsersApi; | ||||||
|   public groups: AdminGroupsApi; |   public groups: AdminGroupsApi; | ||||||
|   public backups: AdminBackupsApi; |   public backups: AdminBackupsApi; | ||||||
|  |   public maintenance: AdminMaintenanceApi; | ||||||
|  |  | ||||||
|   constructor(requests: ApiRequestInstance) { |   constructor(requests: ApiRequestInstance) { | ||||||
|     this.about = new AdminAboutAPI(requests); |     this.about = new AdminAboutAPI(requests); | ||||||
| @@ -18,6 +20,7 @@ export class AdminAPI { | |||||||
|     this.users = new AdminUsersApi(requests); |     this.users = new AdminUsersApi(requests); | ||||||
|     this.groups = new AdminGroupsApi(requests); |     this.groups = new AdminGroupsApi(requests); | ||||||
|     this.backups = new AdminBackupsApi(requests); |     this.backups = new AdminBackupsApi(requests); | ||||||
|  |     this.maintenance = new AdminMaintenanceApi(requests); | ||||||
|  |  | ||||||
|     Object.freeze(this); |     Object.freeze(this); | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										30
									
								
								frontend/api/admin/admin-maintenance.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								frontend/api/admin/admin-maintenance.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | import { BaseAPI } from "../_base"; | ||||||
|  | import { SuccessResponse } from "~/types/api-types/response"; | ||||||
|  | import { MaintenanceSummary } from "~/types/api-types/admin"; | ||||||
|  |  | ||||||
|  | const prefix = "/api"; | ||||||
|  |  | ||||||
|  | const routes = { | ||||||
|  |   base: `${prefix}/admin/maintenance`, | ||||||
|  |   cleanImages: `${prefix}/admin/maintenance/clean/images`, | ||||||
|  |   cleanRecipeFolders: `${prefix}/admin/maintenance/clean/recipe-folders`, | ||||||
|  |   cleanLogFile: `${prefix}/admin/maintenance/clean/logs`, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export class AdminMaintenanceApi extends BaseAPI { | ||||||
|  |   async getInfo() { | ||||||
|  |     return this.requests.get<MaintenanceSummary>(routes.base); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async cleanImages() { | ||||||
|  |     return await this.requests.post<SuccessResponse>(routes.cleanImages, {}); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async cleanRecipeFolders() { | ||||||
|  |     return await this.requests.post<SuccessResponse>(routes.cleanRecipeFolders, {}); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async cleanLogFile() { | ||||||
|  |     return await this.requests.post<SuccessResponse>(routes.cleanLogFile, {}); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -54,6 +54,11 @@ export default defineComponent({ | |||||||
|         to: "/admin/site-settings", |         to: "/admin/site-settings", | ||||||
|         title: i18n.t("sidebar.site-settings"), |         title: i18n.t("sidebar.site-settings"), | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         icon: $globals.icons.cog, | ||||||
|  |         to: "/admin/maintenance", | ||||||
|  |         title: "Maintenance", | ||||||
|  |       }, | ||||||
|       { |       { | ||||||
|         icon: $globals.icons.user, |         icon: $globals.icons.user, | ||||||
|         to: "/admin/manage/users", |         to: "/admin/manage/users", | ||||||
|   | |||||||
							
								
								
									
										165
									
								
								frontend/pages/admin/maintenance.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								frontend/pages/admin/maintenance.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | |||||||
|  | <template> | ||||||
|  |   <v-container fluid class="narrow-container"> | ||||||
|  |     <BasePageTitle divider> | ||||||
|  |       <template #title> Site Maintenance </template> | ||||||
|  |     </BasePageTitle> | ||||||
|  |  | ||||||
|  |     <BannerExperimental /> | ||||||
|  |  | ||||||
|  |     <section> | ||||||
|  |       <BaseCardSectionTitle class="pb-0" :icon="$globals.icons.cog" title="Summary"> </BaseCardSectionTitle> | ||||||
|  |       <div class="mb-6 ml-2"> | ||||||
|  |         <BaseButton color="info" @click="getSummary"> | ||||||
|  |           <template #icon> {{ $globals.icons.tools }} </template> | ||||||
|  |           Get Summary | ||||||
|  |         </BaseButton> | ||||||
|  |       </div> | ||||||
|  |       <v-card class="ma-2" :loading="state.fetchingInfo"> | ||||||
|  |         <template v-for="(value, idx) in info"> | ||||||
|  |           <v-list-item :key="`item-${idx}`"> | ||||||
|  |             <v-list-item-title> | ||||||
|  |               <div>{{ value.name }}</div> | ||||||
|  |             </v-list-item-title> | ||||||
|  |             <v-list-item-subtitle class="text-end"> {{ value.value }} </v-list-item-subtitle> | ||||||
|  |           </v-list-item> | ||||||
|  |           <v-divider :key="`divider-${idx}`" class="mx-2"></v-divider> | ||||||
|  |         </template> | ||||||
|  |       </v-card> | ||||||
|  |     </section> | ||||||
|  |     <section> | ||||||
|  |       <BaseCardSectionTitle class="pb-0 mt-8" :icon="$globals.icons.cog" title="Actions"> | ||||||
|  |         Maintenance actions are <b> destructive </b> and should be used with caution. Performing any of these actions is | ||||||
|  |         <b> irreversible </b>. | ||||||
|  |       </BaseCardSectionTitle> | ||||||
|  |       <v-card class="ma-2" :loading="state.actionLoading"> | ||||||
|  |         <template v-for="(action, idx) in actions"> | ||||||
|  |           <v-list-item :key="`item-${idx}`"> | ||||||
|  |             <v-list-item-title> | ||||||
|  |               <div>{{ action.name }}</div> | ||||||
|  |               <v-list-item-subtitle> | ||||||
|  |                 {{ action.subtitle }} | ||||||
|  |               </v-list-item-subtitle> | ||||||
|  |             </v-list-item-title> | ||||||
|  |             <v-list-item-action> | ||||||
|  |               <BaseButton color="info" @click="action.handler"> | ||||||
|  |                 <template #icon> {{ $globals.icons.robot }}</template> | ||||||
|  |                 Run | ||||||
|  |               </BaseButton> | ||||||
|  |             </v-list-item-action> | ||||||
|  |           </v-list-item> | ||||||
|  |           <v-divider :key="`divider-${idx}`" class="mx-2"></v-divider> | ||||||
|  |         </template> | ||||||
|  |       </v-card> | ||||||
|  |     </section> | ||||||
|  |   </v-container> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import { computed, ref, defineComponent, reactive } from "@nuxtjs/composition-api"; | ||||||
|  | import { useAdminApi } from "~/composables/api"; | ||||||
|  | import { MaintenanceSummary } from "~/types/api-types/admin"; | ||||||
|  |  | ||||||
|  | export default defineComponent({ | ||||||
|  |   layout: "admin", | ||||||
|  |   setup() { | ||||||
|  |     const state = reactive({ | ||||||
|  |       fetchingInfo: false, | ||||||
|  |       actionLoading: false, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const adminApi = useAdminApi(); | ||||||
|  |  | ||||||
|  |     const infoResults = ref<MaintenanceSummary>({ | ||||||
|  |       dataDirSize: "unknown", | ||||||
|  |       logFileSize: "unknown", | ||||||
|  |       cleanableDirs: 0, | ||||||
|  |       cleanableImages: 0, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     async function getSummary() { | ||||||
|  |       state.fetchingInfo = true; | ||||||
|  |       const { data } = await adminApi.maintenance.getInfo(); | ||||||
|  |  | ||||||
|  |       infoResults.value = data ?? { | ||||||
|  |         dataDirSize: "unknown", | ||||||
|  |         logFileSize: "unknown", | ||||||
|  |         cleanableDirs: 0, | ||||||
|  |         cleanableImages: 0, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       state.fetchingInfo = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const info = computed(() => { | ||||||
|  |       return [ | ||||||
|  |         { | ||||||
|  |           name: "Data Directory Size", | ||||||
|  |           value: infoResults.value.dataDirSize, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           name: "Log File Size", | ||||||
|  |           value: infoResults.value.logFileSize, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           name: "Cleanable Directories", | ||||||
|  |           value: infoResults.value.cleanableDirs, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           name: "Cleanable Images", | ||||||
|  |           value: infoResults.value.cleanableImages, | ||||||
|  |         }, | ||||||
|  |       ]; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     async function handleDeleteLogFile() { | ||||||
|  |       state.actionLoading = true; | ||||||
|  |       await adminApi.maintenance.cleanLogFile(); | ||||||
|  |       state.actionLoading = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function handleCleanDirectories() { | ||||||
|  |       state.actionLoading = true; | ||||||
|  |       await adminApi.maintenance.cleanRecipeFolders(); | ||||||
|  |       state.actionLoading = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function handleCleanImages() { | ||||||
|  |       state.actionLoading = true; | ||||||
|  |       await adminApi.maintenance.cleanImages(); | ||||||
|  |       state.actionLoading = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const actions = [ | ||||||
|  |       { | ||||||
|  |         name: "Delete Log Files", | ||||||
|  |         handler: handleDeleteLogFile, | ||||||
|  |         subtitle: "Deletes all the log files", | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: "Clean Directories", | ||||||
|  |         handler: handleCleanDirectories, | ||||||
|  |         subtitle: "Removes all the recipe folders that are not valid UUIDs", | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: "Clean Images", | ||||||
|  |         handler: handleCleanImages, | ||||||
|  |         subtitle: "Removes all the images that don't end with .webp", | ||||||
|  |       }, | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       state, | ||||||
|  |       info, | ||||||
|  |       getSummary, | ||||||
|  |       actions, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   head() { | ||||||
|  |     return { | ||||||
|  |       title: this.$t("settings.site-settings") as string, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped></style> | ||||||
| @@ -190,6 +190,12 @@ export interface ImportJob { | |||||||
|   force?: boolean; |   force?: boolean; | ||||||
|   rebase?: boolean; |   rebase?: boolean; | ||||||
| } | } | ||||||
|  | export interface MaintenanceSummary { | ||||||
|  |   dataDirSize: string; | ||||||
|  |   logFileSize: string; | ||||||
|  |   cleanableImages: number; | ||||||
|  |   cleanableDirs: number; | ||||||
|  | } | ||||||
| export interface MigrationFile { | export interface MigrationFile { | ||||||
|   name: string; |   name: string; | ||||||
|   date: string; |   date: string; | ||||||
|   | |||||||
							
								
								
									
										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: | def pretty_size(size: int) -> str: | ||||||
|     """ |     """ | ||||||
|     Pretty size takes in a integer value of a file size and returns the most applicable |     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" |         return f"{round(size / 1024 / 1024 / 1024, 2)} GB" | ||||||
|     else: |     else: | ||||||
|         return f"{round(size / 1024 / 1024 / 1024 / 1024, 2)} TB" |         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_backups, | ||||||
|     admin_email, |     admin_email, | ||||||
|     admin_log, |     admin_log, | ||||||
|  |     admin_maintenance, | ||||||
|     admin_management_groups, |     admin_management_groups, | ||||||
|     admin_management_users, |     admin_management_users, | ||||||
|     admin_server_tasks, |     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_management_groups.router) | ||||||
| router.include_router(admin_email.router, tags=["Admin: Email"]) | router.include_router(admin_email.router, tags=["Admin: Email"]) | ||||||
| router.include_router(admin_server_tasks.router, tags=["Admin: Server Tasks"]) | 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 | # GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
| from .about import * | from .about import * | ||||||
| from .backup import * | from .backup import * | ||||||
|  | from .maintenance import * | ||||||
| from .migration import * | from .migration import * | ||||||
| from .restore import * | from .restore import * | ||||||
| from .settings 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