mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -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:
parent
b3b1778890
commit
2a158ab290
2
.github/workflows/pytest.yml
vendored
2
.github/workflows/pytest.yml
vendored
@ -11,6 +11,8 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
|
env:
|
||||||
|
PRODUCTION: false
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
#----------------------------------------------
|
#----------------------------------------------
|
||||||
|
@ -20,7 +20,7 @@ RUN apk add --no-cache libxml2-dev \
|
|||||||
zlib-dev
|
zlib-dev
|
||||||
|
|
||||||
|
|
||||||
ENV ENV True
|
ENV PRODUCTION true
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
|
|
||||||
@ -48,6 +48,7 @@ COPY ./dev/data/templates /app/data/templates
|
|||||||
COPY --from=build-stage /app/dist /app/dist
|
COPY --from=build-stage /app/dist /app/dist
|
||||||
|
|
||||||
VOLUME [ "/app/data/" ]
|
VOLUME [ "/app/data/" ]
|
||||||
|
|
||||||
RUN chmod +x /app/mealie/run.sh
|
RUN chmod +x /app/mealie/run.sh
|
||||||
CMD /app/mealie/run.sh
|
CMD /app/mealie/run.sh
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@ FROM python:3
|
|||||||
|
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
|
|
||||||
|
ENV PRODUCTION false
|
||||||
|
|
||||||
RUN apt-get update -y && \
|
RUN apt-get update -y && \
|
||||||
apt-get install -y python-pip python-dev
|
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"
|
- Guidelines: "contributors/developers-guide/general-guidelines.md"
|
||||||
- Development Road Map: "roadmap.md"
|
- Development Road Map: "roadmap.md"
|
||||||
- Change Log:
|
- Change Log:
|
||||||
|
- v0.4.2 Backend/Migrations: "changelog/v0.4.2.md"
|
||||||
- v0.4.1 Frontend/UI: "changelog/v0.4.1.md"
|
- v0.4.1 Frontend/UI: "changelog/v0.4.1.md"
|
||||||
- v0.4.0 Authentication: "changelog/v0.4.0.md"
|
- v0.4.0 Authentication: "changelog/v0.4.0.md"
|
||||||
- v0.3.0 Improvements: "changelog/v0.3.0.md"
|
- v0.3.0 Improvements: "changelog/v0.3.0.md"
|
||||||
|
@ -61,9 +61,16 @@ const apiReq = {
|
|||||||
processResponse(response);
|
processResponse(response);
|
||||||
return 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 { apiReq };
|
||||||
export { baseURL };
|
export { baseURL };
|
||||||
|
@ -4,7 +4,7 @@ import { store } from "@/store";
|
|||||||
|
|
||||||
const backupBase = baseURL + "backups/";
|
const backupBase = baseURL + "backups/";
|
||||||
|
|
||||||
const backupURLs = {
|
export const backupURLs = {
|
||||||
// Backup
|
// Backup
|
||||||
available: `${backupBase}available`,
|
available: `${backupBase}available`,
|
||||||
createBackup: `${backupBase}export/database`,
|
createBackup: `${backupBase}export/database`,
|
||||||
@ -13,6 +13,8 @@ const backupURLs = {
|
|||||||
downloadBackup: fileName => `${backupBase}${fileName}/download`,
|
downloadBackup: fileName => `${backupBase}${fileName}/download`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const backupAPI = {
|
export const backupAPI = {
|
||||||
/**
|
/**
|
||||||
* Request all backups available on the server
|
* Request all backups available on the server
|
||||||
@ -55,7 +57,7 @@ export const backupAPI = {
|
|||||||
* @returns Download URL
|
* @returns Download URL
|
||||||
*/
|
*/
|
||||||
async download(fileName) {
|
async download(fileName) {
|
||||||
let response = await apiReq.get(backupURLs.downloadBackup(fileName));
|
const url = backupURLs.downloadBackup(fileName);
|
||||||
return response.data;
|
apiReq.download(url);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -37,14 +37,7 @@
|
|||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-btn
|
<TheDownloadBtn :download-url="downloadUrl" />
|
||||||
color="accent"
|
|
||||||
text
|
|
||||||
:loading="downloading"
|
|
||||||
@click="downloadFile(`/api/backups/${name}/download`)"
|
|
||||||
>
|
|
||||||
{{ $t("general.download") }}
|
|
||||||
</v-btn>
|
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn color="error" text @click="raiseEvent('delete')">
|
<v-btn color="error" text @click="raiseEvent('delete')">
|
||||||
{{ $t("general.delete") }}
|
{{ $t("general.delete") }}
|
||||||
@ -66,9 +59,10 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ImportOptions from "@/components/Admin/Backup/ImportOptions";
|
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 {
|
export default {
|
||||||
components: { ImportOptions },
|
components: { ImportOptions, TheDownloadBtn },
|
||||||
props: {
|
props: {
|
||||||
name: {
|
name: {
|
||||||
default: "Backup Name",
|
default: "Backup Name",
|
||||||
@ -92,6 +86,11 @@ export default {
|
|||||||
downloading: false,
|
downloading: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
downloadUrl() {
|
||||||
|
return backupURLs.downloadBackup(this.name);
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateOptions(options) {
|
updateOptions(options) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
@ -116,23 +115,6 @@ export default {
|
|||||||
this.close();
|
this.close();
|
||||||
this.$emit(event, eventData);
|
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>
|
</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>
|
||||||
</v-list-item-group>
|
</v-list-item-group>
|
||||||
</v-card-text>
|
</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-divider></v-divider>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
@ -27,7 +38,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
|
import TheDownloadBtn from "@/components/UI/TheDownloadBtn";
|
||||||
export default {
|
export default {
|
||||||
|
components: { TheDownloadBtn },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
prettyInfo: [],
|
prettyInfo: [],
|
||||||
|
@ -5,7 +5,7 @@ from mealie.core import root_logger
|
|||||||
|
|
||||||
# import utils.startup as startup
|
# import utils.startup as startup
|
||||||
from mealie.core.config import APP_VERSION, settings
|
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.groups import groups
|
||||||
from mealie.routes.mealplans import mealplans
|
from mealie.routes.mealplans import mealplans
|
||||||
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes
|
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():
|
def api_routers():
|
||||||
# Authentication
|
# Authentication
|
||||||
|
app.include_router(utility_routes.router)
|
||||||
app.include_router(users.router)
|
app.include_router(users.router)
|
||||||
app.include_router(groups.router)
|
app.include_router(groups.router)
|
||||||
# Recipes
|
# Recipes
|
||||||
@ -36,7 +37,6 @@ def api_routers():
|
|||||||
app.include_router(category_routes.router)
|
app.include_router(category_routes.router)
|
||||||
app.include_router(tag_routes.router)
|
app.include_router(tag_routes.router)
|
||||||
app.include_router(recipe_crud_routes.router)
|
app.include_router(recipe_crud_routes.router)
|
||||||
|
|
||||||
# Meal Routes
|
# Meal Routes
|
||||||
app.include_router(mealplans.router)
|
app.include_router(mealplans.router)
|
||||||
# Settings Routes
|
# Settings Routes
|
||||||
|
@ -3,16 +3,19 @@ import secrets
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
import dotenv
|
||||||
from pydantic import BaseSettings, Field, validator
|
from pydantic import BaseSettings, Field, validator
|
||||||
|
|
||||||
APP_VERSION = "v0.4.1"
|
APP_VERSION = "v0.4.2"
|
||||||
DB_VERSION = "v0.4.0"
|
DB_VERSION = "v0.4.0"
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
BASE_DIR = CWD.parent.parent
|
BASE_DIR = CWD.parent.parent
|
||||||
|
|
||||||
ENV = BASE_DIR.joinpath(".env")
|
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:
|
def determine_data_dir(production: bool) -> Path:
|
||||||
@ -83,7 +86,7 @@ app_dirs = AppDirectories(CWD, DATA_DIR)
|
|||||||
|
|
||||||
class AppSettings(BaseSettings):
|
class AppSettings(BaseSettings):
|
||||||
global DATA_DIR
|
global DATA_DIR
|
||||||
PRODUCTION: bool = Field(False, env="ENV")
|
PRODUCTION: bool = Field(True, env="PRODUCTION")
|
||||||
IS_DEMO: bool = False
|
IS_DEMO: bool = False
|
||||||
API_PORT: int = 9000
|
API_PORT: int = 9000
|
||||||
API_DOCS: bool = True
|
API_DOCS: bool = True
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from mealie.schema.user import UserInDB
|
from pathlib import Path
|
||||||
|
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
from mealie.core.config import settings
|
from mealie.core.config import settings
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
|
from mealie.schema.user import UserInDB
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
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)
|
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:
|
def authenticate_user(session, email: str, password: str) -> UserInDB:
|
||||||
user: UserInDB = db.users.get(session, email, "email")
|
user: UserInDB = db.users.get(session, email, "email")
|
||||||
if not user:
|
if not user:
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import operator
|
import operator
|
||||||
import shutil
|
import shutil
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||||
from mealie.core.config import app_dirs
|
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.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.backup import BackupJob, ImportJob, Imports, LocalBackup
|
||||||
from mealie.schema.snackbar import SnackResponse
|
from mealie.schema.snackbar import SnackResponse
|
||||||
from mealie.services.backups import imports
|
from mealie.services.backups import imports
|
||||||
@ -68,13 +70,10 @@ def upload_backup_file(archive: UploadFile = File(...)):
|
|||||||
|
|
||||||
@router.get("/{file_name}/download")
|
@router.get("/{file_name}/download")
|
||||||
async def download_backup_file(file_name: str):
|
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)
|
file = app_dirs.BACKUP_DIR.joinpath(file_name)
|
||||||
|
|
||||||
if file.is_file:
|
return {"fileToken": create_file_token(file)}
|
||||||
return FileResponse(file, media_type="application/octet-stream", filename=file_name)
|
|
||||||
else:
|
|
||||||
return SnackResponse.error("No File Found")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{file_name}/import", status_code=200)
|
@router.post("/{file_name}/import", status_code=200)
|
||||||
|
@ -3,6 +3,7 @@ import json
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from mealie.core.config import APP_VERSION, app_dirs, settings
|
from mealie.core.config import APP_VERSION, app_dirs, settings
|
||||||
from mealie.core.root_logger import LOGGER_FILE
|
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.routes.deps import get_current_user
|
||||||
from mealie.schema.debug import AppInfo, DebugInfo
|
from mealie.schema.debug import AppInfo, DebugInfo
|
||||||
|
|
||||||
@ -37,10 +38,8 @@ async def get_mealie_version():
|
|||||||
|
|
||||||
@router.get("/last-recipe-json")
|
@router.get("/last-recipe-json")
|
||||||
async def get_last_recipe_json(current_user=Depends(get_current_user)):
|
async def get_last_recipe_json(current_user=Depends(get_current_user)):
|
||||||
""" Doc Str """
|
""" Returns a token to download a file """
|
||||||
|
return {"fileToken": create_file_token(app_dirs.DEBUG_DIR.joinpath("last_recipe.json"))}
|
||||||
with open(app_dirs.DEBUG_DIR.joinpath("last_recipe.json"), "r") as f:
|
|
||||||
return json.loads(f.read())
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/log/{num}")
|
@router.get("/log/{num}")
|
||||||
@ -51,6 +50,12 @@ async def get_log(num: int, current_user=Depends(get_current_user)):
|
|||||||
return log_text
|
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):
|
def tail(f, lines=20):
|
||||||
total_lines_wanted = lines
|
total_lines_wanted = lines
|
||||||
|
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from jose import JWTError, jwt
|
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)
|
token_data = TokenData(username=username)
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
||||||
user = db.users.get(session, token_data.username, "email")
|
user = db.users.get(session, token_data.username, "email")
|
||||||
if user is None:
|
if user is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
return user
|
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")
|
Loading…
x
Reference in New Issue
Block a user