mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-27 08:14:30 -04:00 
			
		
		
		
	feature/debug-info (#286)
* rename 'ENV' to 'PRODUCTION' and default to true * set env PRODUCTION * refactor file download process * add last_recipe.json and log downloads * changelog + version bump * set env on workflows * bump version Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
		
							
								
								
									
										2
									
								
								.github/workflows/pytest.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/pytest.yml
									
									
									
									
										vendored
									
									
								
							| @@ -11,6 +11,8 @@ on: | ||||
|  | ||||
| jobs: | ||||
|   tests: | ||||
|     env: | ||||
|       PRODUCTION: false | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       #---------------------------------------------- | ||||
|   | ||||
| @@ -20,7 +20,7 @@ RUN apk add --no-cache libxml2-dev \ | ||||
|     zlib-dev | ||||
|  | ||||
|  | ||||
| ENV ENV True | ||||
| ENV PRODUCTION true | ||||
| EXPOSE 80 | ||||
| WORKDIR /app/ | ||||
|  | ||||
| @@ -48,6 +48,7 @@ COPY ./dev/data/templates /app/data/templates | ||||
| COPY --from=build-stage /app/dist /app/dist | ||||
|  | ||||
| VOLUME [ "/app/data/" ] | ||||
|  | ||||
| RUN chmod +x /app/mealie/run.sh | ||||
| CMD /app/mealie/run.sh | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,8 @@ FROM python:3 | ||||
|  | ||||
| WORKDIR /app/ | ||||
|  | ||||
| ENV PRODUCTION false | ||||
|  | ||||
| RUN apt-get update -y && \ | ||||
|     apt-get install -y python-pip python-dev | ||||
|  | ||||
|   | ||||
							
								
								
									
										23
									
								
								docs/docs/changelog/v0.4.2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								docs/docs/changelog/v0.4.2.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| # v0.4.2 | ||||
|  | ||||
| **App Version: v0.4.2** | ||||
|  | ||||
| **Database Version: v0.4.0** | ||||
|  | ||||
| !!! error "Breaking Changes" | ||||
|     1. With a recent refactor some users been experiencing issues with an environmental variable not being set correct. If you are experiencing issues, please provide your comments [Here](https://github.com/hay-kot/mealie/issues/281). | ||||
|  | ||||
|     2. If you are a developer, you may experience issues with development as a new environmental variable has been introduced. Setting `PRODUCTION=false` will allow you to develop as normal. | ||||
|  | ||||
| - Improved Nextcloud Migration. Mealie will now walk the directories in a zip file looking for directories that match the pattern of a Nextcloud Recipe. Closes #254 | ||||
|     - Rewrite Keywords to Tag Fields | ||||
|     - Rewrite url to orgURL | ||||
| - Improved Chowdown Migration | ||||
| - Migration report is now similar to the Backup report | ||||
| - Tags/Categories are now title cased on import "dinner" -> "Dinner" | ||||
| - Fixed Initialization script (v0.4.1a Hot Fix) Closes #274 | ||||
| - Depreciate `ENV` variable to `PRODUCTION` | ||||
| - Set `PRODUCTION` env variable to default to true | ||||
| - Unify Logger across the backend | ||||
| - mealie.log and last_recipe.json are now downloadable from the frontend from the /admin/about | ||||
| - New download schema where you request a token and then use that token to hit a single endpoint to download a file. This is a notable change if you are using the API to download backups.  | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -77,6 +77,7 @@ nav: | ||||
|           - Guidelines: "contributors/developers-guide/general-guidelines.md" | ||||
|   - Development Road Map: "roadmap.md" | ||||
|   - Change Log: | ||||
|       - v0.4.2 Backend/Migrations: "changelog/v0.4.2.md" | ||||
|       - v0.4.1 Frontend/UI: "changelog/v0.4.1.md" | ||||
|       - v0.4.0 Authentication: "changelog/v0.4.0.md" | ||||
|       - v0.3.0 Improvements: "changelog/v0.3.0.md" | ||||
|   | ||||
| @@ -61,9 +61,16 @@ const apiReq = { | ||||
|     processResponse(response); | ||||
|     return response; | ||||
|   }, | ||||
|  | ||||
|   async download(url) { | ||||
|     const response = await this.get(url); | ||||
|     const token = response.data.fileToken; | ||||
|  | ||||
|     const tokenURL = baseURL + "utils/download?token=" + token; | ||||
|     window.open(tokenURL, "_blank"); | ||||
|     return response.data; | ||||
|   }, | ||||
| }; | ||||
|  | ||||
|  | ||||
|  | ||||
| export { apiReq }; | ||||
| export { baseURL }; | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { store } from "@/store"; | ||||
|  | ||||
| const backupBase = baseURL + "backups/"; | ||||
|  | ||||
| const backupURLs = { | ||||
| export const backupURLs = { | ||||
|   // Backup | ||||
|   available: `${backupBase}available`, | ||||
|   createBackup: `${backupBase}export/database`, | ||||
| @@ -13,6 +13,8 @@ const backupURLs = { | ||||
|   downloadBackup: fileName => `${backupBase}${fileName}/download`, | ||||
| }; | ||||
|  | ||||
|  | ||||
|  | ||||
| export const backupAPI = { | ||||
|   /** | ||||
|    * Request all backups available on the server | ||||
| @@ -55,7 +57,7 @@ export const backupAPI = { | ||||
|    * @returns Download URL | ||||
|    */ | ||||
|   async download(fileName) { | ||||
|     let response = await apiReq.get(backupURLs.downloadBackup(fileName)); | ||||
|     return response.data; | ||||
|     const url = backupURLs.downloadBackup(fileName); | ||||
|     apiReq.download(url); | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -37,14 +37,7 @@ | ||||
|         <v-divider></v-divider> | ||||
|  | ||||
|         <v-card-actions> | ||||
|           <v-btn | ||||
|             color="accent" | ||||
|             text | ||||
|             :loading="downloading" | ||||
|             @click="downloadFile(`/api/backups/${name}/download`)" | ||||
|           > | ||||
|             {{ $t("general.download") }} | ||||
|           </v-btn> | ||||
|           <TheDownloadBtn :download-url="downloadUrl" /> | ||||
|           <v-spacer></v-spacer> | ||||
|           <v-btn color="error" text @click="raiseEvent('delete')"> | ||||
|             {{ $t("general.delete") }} | ||||
| @@ -66,9 +59,10 @@ | ||||
|  | ||||
| <script> | ||||
| import ImportOptions from "@/components/Admin/Backup/ImportOptions"; | ||||
| import axios from "axios"; | ||||
| import TheDownloadBtn from "@/components/UI/TheDownloadBtn.vue"; | ||||
| import { backupURLs } from "@/api/backup"; | ||||
| export default { | ||||
|   components: { ImportOptions }, | ||||
|   components: { ImportOptions, TheDownloadBtn }, | ||||
|   props: { | ||||
|     name: { | ||||
|       default: "Backup Name", | ||||
| @@ -92,6 +86,11 @@ export default { | ||||
|       downloading: false, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     downloadUrl() { | ||||
|       return backupURLs.downloadBackup(this.name); | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     updateOptions(options) { | ||||
|       this.options = options; | ||||
| @@ -116,23 +115,6 @@ export default { | ||||
|       this.close(); | ||||
|       this.$emit(event, eventData); | ||||
|     }, | ||||
|     async downloadFile(downloadURL) { | ||||
|       this.downloading = true; | ||||
|       const response = await axios({ | ||||
|         url: downloadURL, | ||||
|         method: "GET", | ||||
|         responseType: "blob", // important | ||||
|       }); | ||||
|  | ||||
|       const url = window.URL.createObjectURL(new Blob([response.data])); | ||||
|       const link = document.createElement("a"); | ||||
|       link.href = url; | ||||
|       link.setAttribute("download", `${this.name}.zip`); | ||||
|       document.body.appendChild(link); | ||||
|       link.click(); | ||||
|  | ||||
|       this.downloading = false; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|   | ||||
							
								
								
									
										51
									
								
								frontend/src/components/UI/TheDownloadBtn.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								frontend/src/components/UI/TheDownloadBtn.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| <template> | ||||
|   <v-btn color="accent" text :loading="downloading" @click="downloadFile"> | ||||
|     {{ showButtonText }} | ||||
|   </v-btn> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| /** | ||||
|  * The download button used for the entire site | ||||
|  * pass a URL to the endpoint that will return a | ||||
|  * file_token which will then be used to request the file | ||||
|  * from the server and open that link in a new tab | ||||
|  */ | ||||
| import { apiReq } from "@/api/api-utils"; | ||||
| export default { | ||||
|   props: { | ||||
|     /** | ||||
|      * URL to get token from | ||||
|      */ | ||||
|     downloadUrl: { | ||||
|       default: "", | ||||
|     }, | ||||
|     /** | ||||
|      * Override button text. Defaults to "Download" | ||||
|      */ | ||||
|     buttonText: { | ||||
|       default: null, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       downloading: false, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     showButtonText() { | ||||
|       return this.buttonText || this.$t("general.download"); | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     async downloadFile() { | ||||
|       this.downloading = true; | ||||
|       await apiReq.download(this.downloadUrl); | ||||
|       this.downloading = false; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| </style> | ||||
| @@ -20,6 +20,17 @@ | ||||
|           </v-list-item> | ||||
|         </v-list-item-group> | ||||
|       </v-card-text> | ||||
|       <v-card-actions> | ||||
|         <v-spacer></v-spacer> | ||||
|         <TheDownloadBtn | ||||
|           button-text="Download Recipe JSON" | ||||
|           download-url="/api/debug/last-recipe-json" | ||||
|         /> | ||||
|         <TheDownloadBtn | ||||
|           button-text="Download Log" | ||||
|           download-url="/api/debug/log" | ||||
|         /> | ||||
|       </v-card-actions> | ||||
|       <v-divider></v-divider> | ||||
|     </v-card> | ||||
|   </div> | ||||
| @@ -27,7 +38,9 @@ | ||||
|  | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import TheDownloadBtn from "@/components/UI/TheDownloadBtn"; | ||||
| export default { | ||||
|   components: { TheDownloadBtn }, | ||||
|   data() { | ||||
|     return { | ||||
|       prettyInfo: [], | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from mealie.core import root_logger | ||||
|  | ||||
| # import utils.startup as startup | ||||
| from mealie.core.config import APP_VERSION, settings | ||||
| from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes | ||||
| from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes, utility_routes | ||||
| from mealie.routes.groups import groups | ||||
| from mealie.routes.mealplans import mealplans | ||||
| from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes | ||||
| @@ -29,6 +29,7 @@ def start_scheduler(): | ||||
|  | ||||
| def api_routers(): | ||||
|     # Authentication | ||||
|     app.include_router(utility_routes.router) | ||||
|     app.include_router(users.router) | ||||
|     app.include_router(groups.router) | ||||
|     # Recipes | ||||
| @@ -36,7 +37,6 @@ def api_routers(): | ||||
|     app.include_router(category_routes.router) | ||||
|     app.include_router(tag_routes.router) | ||||
|     app.include_router(recipe_crud_routes.router) | ||||
|  | ||||
|     # Meal Routes | ||||
|     app.include_router(mealplans.router) | ||||
|     # Settings Routes | ||||
|   | ||||
| @@ -3,16 +3,19 @@ import secrets | ||||
| from pathlib import Path | ||||
| from typing import Optional, Union | ||||
|  | ||||
| import dotenv | ||||
| from pydantic import BaseSettings, Field, validator | ||||
|  | ||||
| APP_VERSION = "v0.4.1" | ||||
| APP_VERSION = "v0.4.2" | ||||
| DB_VERSION = "v0.4.0" | ||||
|  | ||||
| CWD = Path(__file__).parent | ||||
| BASE_DIR = CWD.parent.parent | ||||
|  | ||||
| ENV = BASE_DIR.joinpath(".env") | ||||
| PRODUCTION = os.getenv("ENV", "False").lower() in ["true", "1"] | ||||
|  | ||||
| dotenv.load_dotenv(ENV) | ||||
| PRODUCTION = os.getenv("PRODUCTION", "True").lower() in ["true", "1"] | ||||
|  | ||||
|  | ||||
| def determine_data_dir(production: bool) -> Path: | ||||
| @@ -83,7 +86,7 @@ app_dirs = AppDirectories(CWD, DATA_DIR) | ||||
|  | ||||
| class AppSettings(BaseSettings): | ||||
|     global DATA_DIR | ||||
|     PRODUCTION: bool = Field(False, env="ENV") | ||||
|     PRODUCTION: bool = Field(True, env="PRODUCTION") | ||||
|     IS_DEMO: bool = False | ||||
|     API_PORT: int = 9000 | ||||
|     API_DOCS: bool = True | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| from datetime import datetime, timedelta | ||||
| from mealie.schema.user import UserInDB | ||||
| from pathlib import Path | ||||
|  | ||||
| from jose import jwt | ||||
| from mealie.core.config import settings | ||||
| from mealie.db.database import db | ||||
| from mealie.schema.user import UserInDB | ||||
| from passlib.context import CryptContext | ||||
|  | ||||
| pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") | ||||
| @@ -20,6 +21,11 @@ def create_access_token(data: dict(), expires_delta: timedelta = None) -> str: | ||||
|     return jwt.encode(to_encode, settings.SECRET, algorithm=ALGORITHM) | ||||
|  | ||||
|  | ||||
| def create_file_token(file_path: Path) -> bool: | ||||
|     token_data = {"file": str(file_path)} | ||||
|     return create_access_token(token_data, expires_delta=timedelta(minutes=30)) | ||||
|  | ||||
|  | ||||
| def authenticate_user(session, email: str, password: str) -> UserInDB: | ||||
|     user: UserInDB = db.users.get(session, email, "email") | ||||
|     if not user: | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| import operator | ||||
| import shutil | ||||
| from typing import Optional | ||||
|  | ||||
| from fastapi import APIRouter, Depends, File, HTTPException, UploadFile | ||||
| from mealie.core.config import app_dirs | ||||
| from mealie.core.security import create_file_token | ||||
| from mealie.db.db_setup import generate_session | ||||
| from mealie.routes.deps import get_current_user | ||||
| from mealie.routes.deps import get_current_user, validate_file_token | ||||
| from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup | ||||
| from mealie.schema.snackbar import SnackResponse | ||||
| from mealie.services.backups import imports | ||||
| @@ -68,13 +70,10 @@ def upload_backup_file(archive: UploadFile = File(...)): | ||||
|  | ||||
| @router.get("/{file_name}/download") | ||||
| async def download_backup_file(file_name: str): | ||||
|     """ Upload a .zip File to later be imported into Mealie """ | ||||
|     """ Returns a token to download a file """ | ||||
|     file = app_dirs.BACKUP_DIR.joinpath(file_name) | ||||
|  | ||||
|     if file.is_file: | ||||
|         return FileResponse(file, media_type="application/octet-stream", filename=file_name) | ||||
|     else: | ||||
|         return SnackResponse.error("No File Found") | ||||
|     return {"fileToken": create_file_token(file)} | ||||
|  | ||||
|  | ||||
| @router.post("/{file_name}/import", status_code=200) | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import json | ||||
| from fastapi import APIRouter, Depends | ||||
| from mealie.core.config import APP_VERSION, app_dirs, settings | ||||
| from mealie.core.root_logger import LOGGER_FILE | ||||
| from mealie.core.security import create_file_token | ||||
| from mealie.routes.deps import get_current_user | ||||
| from mealie.schema.debug import AppInfo, DebugInfo | ||||
|  | ||||
| @@ -37,10 +38,8 @@ async def get_mealie_version(): | ||||
|  | ||||
| @router.get("/last-recipe-json") | ||||
| async def get_last_recipe_json(current_user=Depends(get_current_user)): | ||||
|     """ Doc Str """ | ||||
|  | ||||
|     with open(app_dirs.DEBUG_DIR.joinpath("last_recipe.json"), "r") as f: | ||||
|         return json.loads(f.read()) | ||||
|     """ Returns a token to download a file """ | ||||
|     return {"fileToken": create_file_token(app_dirs.DEBUG_DIR.joinpath("last_recipe.json"))} | ||||
|  | ||||
|  | ||||
| @router.get("/log/{num}") | ||||
| @@ -51,6 +50,12 @@ async def get_log(num: int, current_user=Depends(get_current_user)): | ||||
|     return log_text | ||||
|  | ||||
|  | ||||
| @router.get("/log") | ||||
| async def get_log_file(): | ||||
|     """ Returns a token to download a file """ | ||||
|     return {"fileToken": create_file_token(LOGGER_FILE)} | ||||
|  | ||||
|  | ||||
| def tail(f, lines=20): | ||||
|     total_lines_wanted = lines | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| from pathlib import Path | ||||
| from typing import Optional | ||||
|  | ||||
| from fastapi import Depends, HTTPException, status | ||||
| from fastapi.security import OAuth2PasswordBearer | ||||
| from jose import JWTError, jwt | ||||
| @@ -25,7 +28,25 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends( | ||||
|         token_data = TokenData(username=username) | ||||
|     except JWTError: | ||||
|         raise credentials_exception | ||||
|  | ||||
|     user = db.users.get(session, token_data.username, "email") | ||||
|     if user is None: | ||||
|         raise credentials_exception | ||||
|     return user | ||||
|  | ||||
|  | ||||
| async def validate_file_token(token: Optional[str] = None) -> Path: | ||||
|     credentials_exception = HTTPException( | ||||
|         status_code=status.HTTP_401_UNAUTHORIZED, | ||||
|         detail="could not validate file token", | ||||
|     ) | ||||
|     if not token: | ||||
|         return None | ||||
|  | ||||
|     try: | ||||
|         payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM]) | ||||
|         file_path = Path(payload.get("file")) | ||||
|     except JWTError: | ||||
|         raise credentials_exception | ||||
|  | ||||
|     return file_path | ||||
|   | ||||
							
								
								
									
										20
									
								
								mealie/routes/utility_routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								mealie/routes/utility_routes.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| from pathlib import Path | ||||
| from typing import Optional | ||||
|  | ||||
| from fastapi import APIRouter, Depends | ||||
| from mealie.routes.deps import validate_file_token | ||||
| from mealie.schema.snackbar import SnackResponse | ||||
| from starlette.responses import FileResponse | ||||
|  | ||||
| router = APIRouter(prefix="/api/utils", tags=["Utils"], include_in_schema=True) | ||||
|  | ||||
|  | ||||
| @router.get("/download") | ||||
| async def download_file(file_path: Optional[Path] = Depends(validate_file_token)): | ||||
|     """ Uses a file token obtained by an active user to retrieve a file from the operating | ||||
|     system. """ | ||||
|     print("File Name:", file_path) | ||||
|     if file_path.is_file(): | ||||
|         return FileResponse(file_path, media_type="application/octet-stream", filename=file_path.name) | ||||
|     else: | ||||
|         return SnackResponse.error("No File Found") | ||||
		Reference in New Issue
	
	Block a user