From 97ae3e7215be9178da4a14bae7157a754a6f5f77 Mon Sep 17 00:00:00 2001 From: hay-kot Date: Sat, 10 Apr 2021 21:15:12 -0800 Subject: [PATCH] refactor file download process --- frontend/src/api/api-utils.js | 11 +++- frontend/src/api/backup.js | 14 ++--- .../components/Admin/Backup/ImportDialog.vue | 36 ++++--------- frontend/src/components/UI/TheDownloadBtn.vue | 51 +++++++++++++++++++ mealie/app.py | 4 +- mealie/core/security.py | 8 ++- mealie/routes/backup_routes.py | 11 ++-- mealie/routes/debug_routes.py | 13 +++-- mealie/routes/deps.py | 21 ++++++++ mealie/routes/utility_routes.py | 20 ++++++++ 10 files changed, 141 insertions(+), 48 deletions(-) create mode 100644 frontend/src/components/UI/TheDownloadBtn.vue create mode 100644 mealie/routes/utility_routes.py diff --git a/frontend/src/api/api-utils.js b/frontend/src/api/api-utils.js index 8c4b0e71d..853d5ac9f 100644 --- a/frontend/src/api/api-utils.js +++ b/frontend/src/api/api-utils.js @@ -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 }; diff --git a/frontend/src/api/backup.js b/frontend/src/api/backup.js index 3ec9a1285..b6afd6e98 100644 --- a/frontend/src/api/backup.js +++ b/frontend/src/api/backup.js @@ -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 @@ -43,19 +45,19 @@ export const backupAPI = { /** * Creates a backup on the serve given a set of options * @param {object} data - * @returns + * @returns */ async create(options) { let response = apiReq.post(backupURLs.createBackup, options); return response; }, /** - * Downloads a file from the server. I don't actually think this is used? - * @param {string} fileName + * Downloads a file from the server. I don't actually think this is used? + * @param {string} fileName * @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); }, }; diff --git a/frontend/src/components/Admin/Backup/ImportDialog.vue b/frontend/src/components/Admin/Backup/ImportDialog.vue index 0d54aeaa5..b379dd982 100644 --- a/frontend/src/components/Admin/Backup/ImportDialog.vue +++ b/frontend/src/components/Admin/Backup/ImportDialog.vue @@ -37,14 +37,7 @@ - - {{ $t("general.download") }} - + {{ $t("general.delete") }} @@ -66,9 +59,10 @@ diff --git a/frontend/src/components/UI/TheDownloadBtn.vue b/frontend/src/components/UI/TheDownloadBtn.vue new file mode 100644 index 000000000..bc13898d6 --- /dev/null +++ b/frontend/src/components/UI/TheDownloadBtn.vue @@ -0,0 +1,51 @@ + + + + + \ No newline at end of file diff --git a/mealie/app.py b/mealie/app.py index 443d8780a..0d16c615b 100644 --- a/mealie/app.py +++ b/mealie/app.py @@ -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 diff --git a/mealie/core/security.py b/mealie/core/security.py index e7a0c8c6a..75758e60c 100644 --- a/mealie/core/security.py +++ b/mealie/core/security.py @@ -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: diff --git a/mealie/routes/backup_routes.py b/mealie/routes/backup_routes.py index da1dac21b..6b41c444e 100644 --- a/mealie/routes/backup_routes.py +++ b/mealie/routes/backup_routes.py @@ -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) diff --git a/mealie/routes/debug_routes.py b/mealie/routes/debug_routes.py index ab062026d..8dc919683 100644 --- a/mealie/routes/debug_routes.py +++ b/mealie/routes/debug_routes.py @@ -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 diff --git a/mealie/routes/deps.py b/mealie/routes/deps.py index a5cd3a65b..c7d308f57 100644 --- a/mealie/routes/deps.py +++ b/mealie/routes/deps.py @@ -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 diff --git a/mealie/routes/utility_routes.py b/mealie/routes/utility_routes.py new file mode 100644 index 000000000..c719b4729 --- /dev/null +++ b/mealie/routes/utility_routes.py @@ -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")