Skip to content

Commit

Permalink
feat: 🚀 Allow uploading images for onboarding pages
Browse files Browse the repository at this point in the history
  • Loading branch information
albinmedoc committed Jul 30, 2024
1 parent 027e665 commit e530d45
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 2 deletions.
3 changes: 2 additions & 1 deletion apps/wizarr-backend/wizarr_backend/api/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .authentication_api import api as authentication_api # REVIEW - This is almost completed
from .backup_api import api as backup_api
from .discord_api import api as discord_api
from .image_api import api as image_api
from .invitations_api import api as invitations_api # REVIEW - This is almost completed
from .libraries_api import api as libraries_api
from .notifications_api import api as notifications_api
Expand Down Expand Up @@ -112,6 +113,7 @@ def handle_request_exception(error):
api.add_namespace(discord_api)
api.add_namespace(emby_api)
api.add_namespace(healthcheck_api)
api.add_namespace(image_api)
api.add_namespace(invitations_api)
api.add_namespace(jellyfin_api)
api.add_namespace(libraries_api)
Expand All @@ -137,4 +139,3 @@ def handle_request_exception(error):

# TODO: Tasks API
# TODO: API API
# TODO: HTML API
85 changes: 85 additions & 0 deletions apps/wizarr-backend/wizarr_backend/api/routes/image_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import os
from json import dumps, loads
from uuid import uuid4
from flask import send_from_directory, current_app, request
from flask_jwt_extended import jwt_required
from flask_restx import Namespace, Resource, reqparse
from werkzeug.utils import secure_filename
from werkzeug.datastructures import FileStorage

api = Namespace("Image", description="Image related operations", path="/image")

# Define the file upload parser
file_upload_parser = reqparse.RequestParser()
file_upload_parser.add_argument('file', location='files',
type=FileStorage, required=True,
help='Image file')

@api.route("")
class ImageListApi(Resource):
"""API resource for all images"""

@jwt_required()
@api.doc(security="jwt")
@api.expect(file_upload_parser)
def post(self):
"""Upload image"""
# Check if the post request has the file part
if 'file' not in request.files:
return {"message": "No file part"}, 400
file = request.files['file']
# If the user does not select a file, the browser submits an
# empty file without a filename.
if file.filename == '':
return {"message": "No selected file"}, 400
if file:
# Extract the file extension
file_extension = os.path.splitext(secure_filename(file.filename))[1].lower()
if file_extension not in ['.png', '.jpg', '.jpeg']:
return {"message": "Unsupported file format"}, 400

upload_folder = current_app.config['UPLOAD_FOLDER']
if not os.path.exists(upload_folder):
os.makedirs(upload_folder)
# Generate a unique filename using UUID
filename = f"{uuid4()}{file_extension}"

# Check if the file exists and generate a new UUID if it does
while os.path.exists(os.path.join(upload_folder, filename)):
filename = f"{uuid4()}{file_extension}"
file_path = os.path.join(upload_folder, filename)
file.save(file_path)
return {"message": f"File {filename} uploaded successfully", "filename": filename}, 201


@api.route("/<filename>")
class ImageAPI(Resource):
"""API resource for a single image"""

@api.response(404, "Image not found")
@api.response(500, "Internal server error")
def get(self, filename):
"""Get image"""
# Assuming images are stored in a directory specified by UPLOAD_FOLDER config
upload_folder = current_app.config['UPLOAD_FOLDER']
image_path = os.path.join(upload_folder, filename)
if os.path.exists(image_path):
return send_from_directory(upload_folder, filename)
else:
return {"message": "Image not found"}, 404

@jwt_required()
@api.doc(description="Delete a single image")
@api.response(404, "Image not found")
@api.response(500, "Internal server error")
def delete(self, filename):
"""Delete image"""
upload_folder = current_app.config['UPLOAD_FOLDER']
image_path = os.path.join(upload_folder, filename)

# Check if the file exists
if not os.path.exists(image_path):
return {"message": "Image not found"}, 404

os.remove(image_path)
return {"message": "Image deleted successfully"}, 200
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<MdEditor v-model="onboardingPage.value" :theme="currentTheme" :preview="false" :language="currentLanguage" :toolbars="toolbars" :footers="['=', 'scrollSwitch']" />
<MdEditor v-model="onboardingPage.value" :theme="currentTheme" :preview="false" :language="currentLanguage" :toolbars="toolbars" :footers="['=', 'scrollSwitch']" @onUploadImg="onUploadImg" />
</template>

<script lang="ts">
Expand All @@ -8,6 +8,7 @@ import { MdEditor } from "md-editor-v3";
import Button from "@/components/Dashboard/Button.vue";
import { useThemeStore } from "@/stores/theme";
import { useLanguageStore } from "@/stores/language";
import { useAxios } from "@/plugins/axios";
import type { Themes, ToolbarNames } from "md-editor-v3";
import type { OnboardingPage } from "@/types/OnboardingPage";
Expand All @@ -33,11 +34,32 @@ export default defineComponent({
const toolbars = ref<ToolbarNames[]>(["bold", "underline", "italic", "-", "title", "strikeThrough", "sub", "sup", "quote", "-", "codeRow", "code", "link", "image", "table", "=", "preview", "pageFullscreen"]);
const axios = useAxios();
const onUploadImg = async (files: File[], callback: (files: string[]) => unknown) => {
const res = await Promise.all(
files.map((file) => {
const form = new FormData();
form.append("file", file);
return axios.post("/api/image", form, {
headers: {
"Content-Type": "multipart/form-data",
},
});
}),
);
callback(
res.map((item) => {
return `${window.location.protocol}//${window.location.host}/api/image/${item.data.filename}`;
}),
);
};
return {
currentTheme: currentTheme as unknown as Themes,
currentLanguage,
onboardingPage: props.onboardingPage,
toolbars,
onUploadImg,
};
},
});
Expand Down

0 comments on commit e530d45

Please sign in to comment.