Coverage for opt/mealie/lib/python3.12/site-packages/mealie/routes/admin/admin_maintenance.py: 27%

74 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-11-25 15:48 +0000

1import shutil 1a

2import uuid 1a

3from pathlib import Path 1a

4 

5from fastapi import APIRouter, HTTPException 1a

6 

7from mealie.pkgs.stats import fs_stats 1a

8from mealie.routes._base import BaseAdminController, controller 1a

9from mealie.schema.admin import MaintenanceSummary 1a

10from mealie.schema.admin.maintenance import MaintenanceStorageDetails 1a

11from mealie.schema.response import ErrorResponse, SuccessResponse 1a

12 

13router = APIRouter(prefix="/maintenance") 1a

14 

15 

16def clean_images(root_dir: Path, dry_run: bool) -> int: 1a

17 cleaned_images = 0 

18 

19 for recipe_dir in root_dir.iterdir(): 

20 image_dir = recipe_dir.joinpath("images") 

21 

22 if not image_dir.exists(): 

23 continue 

24 

25 for image in image_dir.iterdir(): 

26 if image.is_dir(): 

27 continue 

28 

29 if image.suffix != ".webp": 

30 if not dry_run: 

31 image.unlink() 

32 

33 cleaned_images += 1 

34 

35 return cleaned_images 

36 

37 

38def clean_recipe_folders(root_dir: Path, dry_run: bool) -> int: 1a

39 cleaned_dirs = 0 

40 

41 for recipe_dir in root_dir.iterdir(): 

42 if recipe_dir.is_dir(): 

43 # Attempt to convert the folder name to a UUID 

44 try: 

45 uuid.UUID(recipe_dir.name) 

46 continue 

47 except ValueError: 

48 if not dry_run: 

49 shutil.rmtree(recipe_dir) 

50 cleaned_dirs += 1 

51 

52 return cleaned_dirs 

53 

54 

55def tail_log(log_file: Path, n: int) -> list[str]: 1a

56 try: 

57 with open(log_file) as f: 

58 lines = f.readlines() 

59 except FileNotFoundError: 

60 return ["no log file found"] 

61 

62 return lines[-n:] 

63 

64 

65@controller(router) 1a

66class AdminMaintenanceController(BaseAdminController): 1a

67 @router.get("", response_model=MaintenanceSummary) 1a

68 def get_maintenance_summary(self): 1a

69 """ 

70 Get the maintenance summary 

71 """ 

72 

73 return MaintenanceSummary( 

74 data_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.folders.DATA_DIR)), 

75 cleanable_images=clean_images(self.folders.RECIPE_DATA_DIR, dry_run=True), 

76 cleanable_dirs=clean_recipe_folders(self.folders.RECIPE_DATA_DIR, dry_run=True), 

77 ) 

78 

79 @router.get("/storage", response_model=MaintenanceStorageDetails) 1a

80 def get_storage_details(self): 1a

81 return MaintenanceStorageDetails( 

82 temp_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.folders.TEMP_DIR)), 

83 backups_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.folders.BACKUP_DIR)), 

84 groups_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.folders.GROUPS_DIR)), 

85 recipes_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.folders.RECIPE_DATA_DIR)), 

86 user_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.folders.USER_DIR)), 

87 ) 

88 

89 @router.post("/clean/images", response_model=SuccessResponse) 1a

90 def clean_images(self): 1a

91 """ 

92 Purges all the images from the filesystem that aren't .webp 

93 """ 

94 try: 

95 cleaned_images = clean_images(self.folders.RECIPE_DATA_DIR, dry_run=False) 

96 return SuccessResponse.respond(f"{cleaned_images} Images cleaned") 

97 except Exception as e: 

98 raise HTTPException(status_code=500, detail=ErrorResponse.respond("Failed to clean images")) from e 

99 

100 @router.post("/clean/temp", response_model=SuccessResponse) 1a

101 def clean_temp(self): 1a

102 try: 

103 if self.folders.TEMP_DIR.exists(): 

104 shutil.rmtree(self.folders.TEMP_DIR) 

105 

106 self.folders.TEMP_DIR.mkdir(parents=True, exist_ok=True) 

107 except Exception as e: 

108 raise HTTPException(status_code=500, detail=ErrorResponse.respond("Failed to clean temp")) from e 

109 

110 return SuccessResponse.respond("'.temp' directory cleaned") 

111 

112 @router.post("/clean/recipe-folders", response_model=SuccessResponse) 1a

113 def clean_recipe_folders(self): 1a

114 """ 

115 Deletes all the recipe folders that don't have names that are valid UUIDs 

116 """ 

117 try: 

118 cleaned_dirs = clean_recipe_folders(self.folders.RECIPE_DATA_DIR, dry_run=False) 

119 return SuccessResponse.respond(f"{cleaned_dirs} Recipe folders removed") 

120 except Exception as e: 

121 raise HTTPException(status_code=500, detail=ErrorResponse.respond("Failed to clean directories")) from e