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:
|
||||
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
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
|
@ -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")
|
Loading…
x
Reference in New Issue
Block a user