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:
Hayden 2021-04-10 21:42:04 -08:00 committed by GitHub
parent b3b1778890
commit 2a158ab290
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 191 additions and 53 deletions

View File

@ -11,6 +11,8 @@ on:
jobs:
tests:
env:
PRODUCTION: false
runs-on: ubuntu-latest
steps:
#----------------------------------------------

View File

@ -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

View File

@ -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

View 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

View File

@ -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"

View File

@ -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 };

View File

@ -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);
},
};

View File

@ -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>

View 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>

View File

@ -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: [],

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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

View 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")