mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 06:23:34 -07:00
refactor file download process
This commit is contained in:
parent
b6969c4fff
commit
97ae3e7215
10 changed files with 141 additions and 48 deletions
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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…
Add table
Add a link
Reference in a new issue