mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 06:23:34 -07:00
Upload component (#108)
* unified upload button + download backups * javascript toolings * fix vuetur config * fixed type check error * refactor: clean up bag javascript Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
parent
51893e89cd
commit
f35e9c20d6
19 changed files with 239 additions and 105 deletions
3
frontend/jsconfig.json
Normal file
3
frontend/jsconfig.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"include": ["./src/**/*"]
|
||||
}
|
|
@ -54,5 +54,11 @@
|
|||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
],
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
}
|
||||
}
|
|
@ -34,11 +34,11 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Menu from "./components/UI/Menu";
|
||||
import SearchBar from "./components/UI/SearchBar";
|
||||
import AddRecipeFab from "./components/UI/AddRecipeFab";
|
||||
import SnackBar from "./components/UI/SnackBar";
|
||||
import Vuetify from "./plugins/vuetify";
|
||||
import Menu from "./components/UI/Menu"
|
||||
import SearchBar from "./components/UI/SearchBar"
|
||||
import AddRecipeFab from "./components/UI/AddRecipeFab"
|
||||
import SnackBar from "./components/UI/SnackBar"
|
||||
import Vuetify from "./plugins/vuetify"
|
||||
export default {
|
||||
name: "App",
|
||||
|
||||
|
@ -51,15 +51,15 @@ export default {
|
|||
|
||||
watch: {
|
||||
$route() {
|
||||
this.search = false;
|
||||
this.search = false
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$store.dispatch("initTheme");
|
||||
this.$store.dispatch("requestRecentRecipes");
|
||||
this.darkModeSystemCheck();
|
||||
this.darkModeAddEventListener();
|
||||
this.$store.dispatch("initTheme")
|
||||
this.$store.dispatch("requestRecentRecipes")
|
||||
this.darkModeSystemCheck()
|
||||
this.darkModeAddEventListener()
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
|
@ -73,30 +73,30 @@ export default {
|
|||
if (this.$store.getters.getDarkMode === "system")
|
||||
Vuetify.framework.theme.dark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
).matches
|
||||
},
|
||||
/**
|
||||
* This will monitor the OS level darkmode and call to update dark mode.
|
||||
*/
|
||||
darkModeAddEventListener() {
|
||||
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
darkMediaQuery.addEventListener("change", () => {
|
||||
this.darkModeSystemCheck();
|
||||
});
|
||||
this.darkModeSystemCheck()
|
||||
})
|
||||
},
|
||||
|
||||
toggleSearch() {
|
||||
if (this.search === true) {
|
||||
this.search = false;
|
||||
this.search = false
|
||||
} else {
|
||||
this.search = true;
|
||||
this.search = true
|
||||
}
|
||||
},
|
||||
navigateFromSearch(slug) {
|
||||
this.$router.push(`/recipe/${slug}`);
|
||||
this.$router.push(`/recipe/${slug}`)
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -4,8 +4,7 @@ import mealplan from "./api/mealplan";
|
|||
import settings from "./api/settings";
|
||||
import themes from "./api/themes";
|
||||
import migration from "./api/migration";
|
||||
|
||||
// import api from "../api";
|
||||
import myUtils from "./api/upload";
|
||||
|
||||
export default {
|
||||
recipes: recipe,
|
||||
|
@ -14,4 +13,5 @@ export default {
|
|||
settings: settings,
|
||||
themes: themes,
|
||||
migrations: migration,
|
||||
utils: myUtils,
|
||||
};
|
||||
|
|
|
@ -16,8 +16,8 @@ function processResponse(response) {
|
|||
}
|
||||
|
||||
const apiReq = {
|
||||
post: async function(url, data) {
|
||||
let response = await axios.post(url, data).catch(function(error) {
|
||||
post: async function (url, data) {
|
||||
let response = await axios.post(url, data).catch(function (error) {
|
||||
if (error.response) {
|
||||
processResponse(error.response);
|
||||
return error.response;
|
||||
|
@ -27,8 +27,8 @@ const apiReq = {
|
|||
return response;
|
||||
},
|
||||
|
||||
get: async function(url, data) {
|
||||
let response = await axios.get(url, data).catch(function(error) {
|
||||
get: async function (url, data) {
|
||||
let response = await axios.get(url, data).catch(function (error) {
|
||||
if (error.response) {
|
||||
processResponse(error.response);
|
||||
return response;
|
||||
|
@ -38,8 +38,8 @@ const apiReq = {
|
|||
return response;
|
||||
},
|
||||
|
||||
delete: async function(url, data) {
|
||||
let response = await axios.delete(url, data).catch(function(error) {
|
||||
delete: async function (url, data) {
|
||||
let response = await axios.delete(url, data).catch(function (error) {
|
||||
if (error.response) {
|
||||
processResponse(error.response);
|
||||
return response;
|
||||
|
|
|
@ -10,6 +10,7 @@ const backupURLs = {
|
|||
createBackup: `${backupBase}export/database/`,
|
||||
importBackup: (fileName) => `${backupBase}${fileName}/import/`,
|
||||
deleteBackup: (fileName) => `${backupBase}${fileName}/delete/`,
|
||||
downloadBackup: (fileName) => `${backupBase}${fileName}/download/`,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -32,4 +33,8 @@ export default {
|
|||
let response = apiReq.post(backupURLs.createBackup, data);
|
||||
return response;
|
||||
},
|
||||
async download(fileName) {
|
||||
let response = await apiReq.get(backupURLs.downloadBackup(fileName));
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
13
frontend/src/api/upload.js
Normal file
13
frontend/src/api/upload.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
|
||||
export default {
|
||||
// import api from "../api";
|
||||
async uploadFile(url, fileObject) {
|
||||
let response = await apiReq.post(url, fileObject, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
};
|
|
@ -1,13 +1,17 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="headline"> {{$t('meal-plan.edit-meal-plan')}} </v-card-title>
|
||||
<v-card-title class="headline">
|
||||
{{ $t("meal-plan.edit-meal-plan") }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
<MealPlanCard v-model="mealPlan.meals" />
|
||||
<v-row align="center" justify="end">
|
||||
<v-card-actions>
|
||||
<v-btn color="success" text @click="update"> {{$t('general.update')}} </v-btn>
|
||||
<v-btn color="success" text @click="update">
|
||||
{{ $t("general.update") }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
</v-card-actions>
|
||||
</v-row>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
{{$t('meal-plan.create-a-new-meal-plan')}}
|
||||
{{ $t("meal-plan.create-a-new-meal-plan") }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
|
@ -71,9 +71,11 @@
|
|||
<v-row align="center" justify="end">
|
||||
<v-card-actions>
|
||||
<v-btn color="success" @click="random" v-if="meals[1]" text>
|
||||
{{$t('general.random')}}
|
||||
{{ $t("general.random") }}
|
||||
</v-btn>
|
||||
<v-btn color="success" @click="save" text>
|
||||
{{ $t("general.save") }}
|
||||
</v-btn>
|
||||
<v-btn color="success" @click="save" text> {{$t('general.save')}} </v-btn>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="show = !show"> </v-btn>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col >
|
||||
<v-col>
|
||||
<v-checkbox
|
||||
class="mb-n4 mt-1"
|
||||
dense
|
||||
|
@ -65,15 +65,15 @@
|
|||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn disabled color="success" text @click="raiseEvent('download')">
|
||||
{{$t('general.download')}}
|
||||
<v-btn color="success" text :href="`/api/backups/${name}/download/`">
|
||||
{{ $t("general.download") }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" text @click="raiseEvent('delete')">
|
||||
{{$t('general.delete')}}
|
||||
{{ $t("general.delete") }}
|
||||
</v-btn>
|
||||
<v-btn color="success" text @click="raiseEvent('import')">
|
||||
{{$t('general.import')}}
|
||||
{{ $t("general.import") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
|
|
@ -21,10 +21,11 @@
|
|||
Available Backups
|
||||
<v-spacer></v-spacer>
|
||||
<span>
|
||||
<v-btn color="success" text class="ma-2 white--text">
|
||||
Upload
|
||||
<v-icon right dark> mdi-cloud-upload </v-icon>
|
||||
</v-btn>
|
||||
<UploadBtn
|
||||
class="mt-1"
|
||||
url="/api/backups/upload/"
|
||||
@uploaded="getAvailableBackups"
|
||||
/>
|
||||
</span>
|
||||
</v-card-title>
|
||||
<AvailableBackupCard
|
||||
|
@ -45,12 +46,14 @@
|
|||
<script>
|
||||
import api from "../../../api";
|
||||
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
||||
import UploadBtn from "../../UI/UploadBtn";
|
||||
import AvailableBackupCard from "./AvailableBackupCard";
|
||||
import NewBackupCard from "./NewBackupCard";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SuccessFailureAlert,
|
||||
UploadBtn,
|
||||
AvailableBackupCard,
|
||||
NewBackupCard,
|
||||
},
|
||||
|
@ -70,6 +73,7 @@ export default {
|
|||
let response = await api.backups.requestAvailable();
|
||||
this.availableBackups = response.imports;
|
||||
this.availableTemplates = response.templates;
|
||||
console.log(this.availableBackups);
|
||||
},
|
||||
deleteBackup() {
|
||||
if (this.$refs.form.validate()) {
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
<template>
|
||||
<v-card-text>
|
||||
<p>
|
||||
{{$t('migration.you-can-import-recipes-from-either-a-zip-file-or-a-directory-located-in-the-app-data-migraiton-folder-please-review-the-documentation-to-ensure-your-directory-structure-matches-what-is-expected')}}
|
||||
{{
|
||||
$t(
|
||||
"migration.you-can-import-recipes-from-either-a-zip-file-or-a-directory-located-in-the-app-data-migraiton-folder-please-review-the-documentation-to-ensure-your-directory-structure-matches-what-is-expected"
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<v-form ref="form">
|
||||
<v-row align="center">
|
||||
|
@ -13,27 +17,32 @@
|
|||
:rules="[rules.required]"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2" sm="12">
|
||||
<v-btn text color="info" @click="importRecipes"> {{$t('migration.migrate')}} </v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="1" sm="12">
|
||||
<v-btn text color="error" @click="deleteImportValidation">
|
||||
{{$t('general.delete')}}
|
||||
</v-btn>
|
||||
<Confirmation
|
||||
:title="$t('general.delete-data')"
|
||||
:message="$t('migration.delete-confirmation')"
|
||||
color="error"
|
||||
icon="mdi-alert-circle"
|
||||
ref="deleteThemeConfirm"
|
||||
v-on:confirm="deleteImport()"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="5" sm="12">
|
||||
<UploadMigrationButton @uploaded="getAvaiableImports" />
|
||||
<v-col md="1" sm="12">
|
||||
<v-btn-toggle group>
|
||||
<v-btn text color="info" @click="importRecipes">
|
||||
{{ $t("migration.migrate") }}
|
||||
</v-btn>
|
||||
<v-btn text color="error" @click="deleteImportValidation">
|
||||
{{ $t("general.delete") }}
|
||||
</v-btn>
|
||||
<UploadBtn
|
||||
url="/api/migration/upload/"
|
||||
class="mt-1"
|
||||
@uploaded="getAvaiableImports"
|
||||
/>
|
||||
|
||||
<Confirmation
|
||||
:title="$t('general.delete-data')"
|
||||
:message="$t('migration.delete-confirmation')"
|
||||
color="error"
|
||||
icon="mdi-alert-circle"
|
||||
ref="deleteThemeConfirm"
|
||||
v-on:confirm="deleteImport()"
|
||||
/>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<SuccessFailureAlert
|
||||
|
@ -48,13 +57,13 @@
|
|||
<script>
|
||||
import api from "../../../api";
|
||||
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
||||
import UploadMigrationButton from "./UploadMigrationButton";
|
||||
import Confirmation from "../../UI/Confirmation";
|
||||
import UploadBtn from "../../UI/UploadBtn";
|
||||
export default {
|
||||
components: {
|
||||
SuccessFailureAlert,
|
||||
UploadMigrationButton,
|
||||
Confirmation,
|
||||
UploadBtn,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -63,7 +72,7 @@ export default {
|
|||
availableImports: [],
|
||||
selectedImport: null,
|
||||
rules: {
|
||||
required: (v) => !!v || "Selection Required",
|
||||
required: v => !!v || "Selection Required",
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
<v-form ref="form" lazy-validation>
|
||||
<v-row dense align="center">
|
||||
<v-col cols="12" md="4" sm="3">
|
||||
<v-col md="4" sm="3">
|
||||
<v-select
|
||||
:label="$t('settings.theme.saved-color-theme')"
|
||||
:items="availableThemes"
|
||||
|
@ -51,18 +51,18 @@
|
|||
return-object
|
||||
v-model="selectedTheme"
|
||||
@change="themeSelected"
|
||||
:rules="[(v) => !!v || $t('settings.theme.theme-is-required')]"
|
||||
:rules="[v => !!v || $t('settings.theme.theme-is-required')]"
|
||||
required
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="1">
|
||||
<NewThemeDialog @new-theme="appendTheme" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="1">
|
||||
<v-btn text color="error" @click="deleteSelectedThemeValidation">
|
||||
Delete
|
||||
</v-btn>
|
||||
<v-col>
|
||||
<v-btn-toggle group>
|
||||
<NewThemeDialog @new-theme="appendTheme" class="mt-1" />
|
||||
<v-btn text color="error" @click="deleteSelectedThemeValidation">
|
||||
Delete
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<Confirmation
|
||||
:title="$t('settings.theme.delete-theme')"
|
||||
:message="
|
||||
|
@ -74,6 +74,7 @@
|
|||
v-on:confirm="deleteSelectedTheme()"
|
||||
/>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<v-row dense align-content="center" v-if="selectedTheme.colors">
|
||||
|
@ -123,15 +124,10 @@
|
|||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-row>
|
||||
<v-col> </v-col>
|
||||
<v-col></v-col>
|
||||
<v-col align="end">
|
||||
<v-btn text color="success" @click="saveThemes">
|
||||
{{ $t("settings.theme.save-colors-and-apply-theme") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" @click="saveThemes" class="mr-2">
|
||||
{{ $t("general.save") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
@ -187,7 +183,7 @@ export default {
|
|||
//Change to default if deleting current theme.
|
||||
if (
|
||||
!this.availableThemes.some(
|
||||
(theme) => theme.name === this.selectedTheme.name
|
||||
theme => theme.name === this.selectedTheme.name
|
||||
)
|
||||
) {
|
||||
await this.$store.dispatch("resetTheme");
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
{{$t('settings.webhooks.meal-planner-webhooks')}}
|
||||
{{ $t("settings.webhooks.meal-planner-webhooks") }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p v-html="$t('settings.webhooks.the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at', {time: time})"></p>
|
||||
<p
|
||||
v-html="
|
||||
$t(
|
||||
'settings.webhooks.the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at',
|
||||
{ time: time }
|
||||
)
|
||||
"
|
||||
></p>
|
||||
|
||||
<v-row dense align="center">
|
||||
<v-col cols="12" md="2" sm="5">
|
||||
|
@ -19,7 +26,9 @@
|
|||
<TimePickerDialog @save-time="saveTime" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="5">
|
||||
<v-btn text color="info" @click="testWebhooks"> {{$t('settings.webhooks.test-webhooks')}} </v-btn>
|
||||
<v-btn text color="info" @click="testWebhooks">
|
||||
{{ $t("settings.webhooks.test-webhooks") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
@ -46,8 +55,8 @@
|
|||
</v-col>
|
||||
<v-col> </v-col>
|
||||
<v-col align="end">
|
||||
<v-btn text color="success" @click="saveWebhooks">
|
||||
{{$t('settings.webhooks.save-webhooks')}}
|
||||
<v-btn color="success" @click="saveWebhooks" class="mr-2 mb-1">
|
||||
{{ $t("settings.webhooks.save-webhooks") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
|
|
@ -1,24 +1,61 @@
|
|||
<template>
|
||||
<v-form ref="file">
|
||||
<v-file-input
|
||||
:loading="loading"
|
||||
:label="$t('migration.upload-an-archive')"
|
||||
v-model="file"
|
||||
accept=".zip"
|
||||
@change="upload"
|
||||
:prepend-icon="icon"
|
||||
class="file-icon"
|
||||
<input ref="uploader" class="d-none" type="file" @change="onFileChanged" />
|
||||
<v-btn
|
||||
:loading="isSelecting"
|
||||
@click="onButtonClick"
|
||||
color="success"
|
||||
text
|
||||
>
|
||||
</v-file-input>
|
||||
<v-btn color="success" text class="ma-2 white--text">
|
||||
<v-icon left > mdi-cloud-upload </v-icon>
|
||||
Upload
|
||||
<v-icon right dark> mdi-cloud-upload </v-icon>
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
import api from "../../api";
|
||||
export default {
|
||||
props: {
|
||||
url: String,
|
||||
},
|
||||
data: () => ({
|
||||
defaultButtonText: "Upload",
|
||||
file: null,
|
||||
isSelecting: false,
|
||||
}),
|
||||
|
||||
methods: {
|
||||
async upload() {
|
||||
if (this.file != null) {
|
||||
this.isSelecting = true;
|
||||
let formData = new FormData();
|
||||
formData.append("archive", this.file);
|
||||
|
||||
await api.utils.uploadFile(this.url, formData);
|
||||
|
||||
this.isSelecting = false;
|
||||
this.$emit("uploaded");
|
||||
}
|
||||
},
|
||||
onButtonClick() {
|
||||
this.isSelecting = true;
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
() => {
|
||||
this.isSelecting = false;
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
this.$refs.uploader.click();
|
||||
},
|
||||
onFileChanged(e) {
|
||||
this.file = e.target.files[0];
|
||||
this.upload();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Paste in your recipe data. Each line will be treated as an item in a list"
|
||||
},
|
||||
"general": {
|
||||
"upload": "Upload",
|
||||
"submit": "Submit",
|
||||
"name": "Name",
|
||||
"settings": "Settings",
|
||||
|
@ -137,4 +138,4 @@
|
|||
"failed-imports": "Failed Imports",
|
||||
"upload-an-archive": "Upload an Archive"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -109,7 +109,7 @@ export default {
|
|||
},
|
||||
|
||||
editPlan(id) {
|
||||
this.plannedMeals.forEach((element) => {
|
||||
this.plannedMeals.forEach(element => {
|
||||
if (element.uid === id) {
|
||||
this.editMealPlan = element;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import operator
|
||||
import shutil
|
||||
|
||||
from app_config import BACKUP_DIR, TEMPLATE_DIR
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from models.backup_models import BackupJob, ImportJob, Imports, LocalBackup
|
||||
from services.backups.exports import backup_all
|
||||
from services.backups.imports import ImportDatabase
|
||||
from sqlalchemy.orm.session import Session
|
||||
from starlette.responses import FileResponse
|
||||
from utils.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(tags=["Import / Export"])
|
||||
|
@ -49,6 +51,33 @@ def export_database(data: BackupJob, db: Session = Depends(generate_session)):
|
|||
)
|
||||
|
||||
|
||||
@router.post("/api/backups/upload/")
|
||||
def upload_backup_zipfile(archive: UploadFile = File(...)):
|
||||
""" Upload a .zip File to later be imported into Mealie """
|
||||
dest = BACKUP_DIR.joinpath(archive.filename)
|
||||
|
||||
with dest.open("wb") as buffer:
|
||||
shutil.copyfileobj(archive.file, buffer)
|
||||
|
||||
if dest.is_file:
|
||||
return SnackResponse.success("Backup uploaded")
|
||||
else:
|
||||
return SnackResponse.error("Failure uploading file")
|
||||
|
||||
|
||||
@router.get("/api/backups/{file_name}/download/")
|
||||
def upload_nextcloud_zipfile(file_name: str):
|
||||
""" Upload a .zip File to later be imported into Mealie """
|
||||
file = 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")
|
||||
|
||||
|
||||
@router.post("/api/backups/{file_name}/import/", status_code=200)
|
||||
def import_database(
|
||||
file_name: str, import_data: ImportJob, db: Session = Depends(generate_session)
|
||||
|
|
16
vetur.config.js
Normal file
16
vetur.config.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
// vetur.config.js
|
||||
/** @type {import('vls').VeturConfig} */
|
||||
module.exports = {
|
||||
settings: {
|
||||
"vetur.useWorkspaceDependencies": true,
|
||||
"vetur.experimental.templateInterpolationService": true,
|
||||
"vetur.validation.interpolation": false,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
root: "./frontend",
|
||||
package: "package.json",
|
||||
globalComponents: ["./src/components/**/*.vue"],
|
||||
},
|
||||
],
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue