✨ Migrated from image urls to image uuids
This commit is contained in:
@@ -398,3 +398,35 @@ class StorageItem(ormar.Model):
|
|||||||
tablename = "storage_items"
|
tablename = "storage_items"
|
||||||
metadata = metadata
|
metadata = metadata
|
||||||
database = database
|
database = database
|
||||||
|
|
||||||
|
|
||||||
|
class PublicStorageItem(BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
uploaded_at: datetime
|
||||||
|
mime_type: str
|
||||||
|
hash: str | None
|
||||||
|
size: int
|
||||||
|
deleted_at: datetime | None
|
||||||
|
alt_text: str | None
|
||||||
|
filename: str | None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_db_model(cls, data: StorageItem):
|
||||||
|
hash_data = None
|
||||||
|
if data.hash is not None:
|
||||||
|
hash_data = data.hash.hex()
|
||||||
|
return cls(
|
||||||
|
id=data.id,
|
||||||
|
uploaded_at=data.uploaded_at,
|
||||||
|
mime_type=data.mime_type,
|
||||||
|
hash=hash_data,
|
||||||
|
size=data.size,
|
||||||
|
deleted_at=data.deleted_at,
|
||||||
|
alt_text=data.alt_text,
|
||||||
|
filename=data.filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateStorageItem(BaseModel):
|
||||||
|
filename: str
|
||||||
|
alt_text: str
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import uuid
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import ormar.exceptions
|
import ormar.exceptions
|
||||||
@@ -175,3 +176,23 @@ def check_hashcash(data: str, input_data: str, claim_in: Optional[str] = "19") -
|
|||||||
return False
|
return False
|
||||||
some_error = [version == "1", claim == claim_in, res == input_data, ext == ""]
|
some_error = [version == "1", claim == claim_in, res == input_data, ext == ""]
|
||||||
return all(el is True for el in some_error)
|
return all(el is True for el in some_error)
|
||||||
|
|
||||||
|
|
||||||
|
def check_image_string(image: str) -> (bool, uuid.UUID | None):
|
||||||
|
# Valid formats: {uuid} and {uuid}--{uuid}
|
||||||
|
try:
|
||||||
|
parsed_uuid = uuid.UUID(image)
|
||||||
|
return True, parsed_uuid
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
split_image = image.split("--")
|
||||||
|
if len(split_image) != 2:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
try:
|
||||||
|
uuid.UUID(split_image[0])
|
||||||
|
uuid.UUID(split_image[1])
|
||||||
|
return True, None
|
||||||
|
except ValueError:
|
||||||
|
return False, None
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
|
|||||||
if q.image is not None and q.image != "":
|
if q.image is not None and q.image != "":
|
||||||
image_bytes = await _download_image(q.image)
|
image_bytes = await _download_image(q.image)
|
||||||
image_name = f"{quiz_id}--{uuid.uuid4()}"
|
image_name = f"{quiz_id}--{uuid.uuid4()}"
|
||||||
image = await storage.upload(file_name=image_name, file_data=image_bytes)
|
await storage.upload(file_name=image_name, file_data=image_bytes)
|
||||||
image = f"{settings.root_address}/api/v1/storage/download/{image_name}"
|
image = image_name
|
||||||
for i, a in enumerate(q.choices):
|
for i, a in enumerate(q.choices):
|
||||||
answers.append(
|
answers.append(
|
||||||
(
|
(
|
||||||
@@ -74,7 +74,7 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
|
|||||||
image_bytes = await _download_image(quiz.kahoot.cover)
|
image_bytes = await _download_image(quiz.kahoot.cover)
|
||||||
image_name = f"{quiz_id}--{uuid.uuid4()}"
|
image_name = f"{quiz_id}--{uuid.uuid4()}"
|
||||||
await storage.upload(file_name=image_name, file_data=image_bytes)
|
await storage.upload(file_name=image_name, file_data=image_bytes)
|
||||||
cover = f"{settings.root_address}/api/v1/storage/download/{image_name}"
|
cover = image_name
|
||||||
quiz_data = Quiz(
|
quiz_data = Quiz(
|
||||||
id=quiz_id,
|
id=quiz_id,
|
||||||
public=True,
|
public=True,
|
||||||
|
|||||||
+10
-49
@@ -4,24 +4,22 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import html
|
import html
|
||||||
import re
|
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import asyncpg.exceptions
|
import asyncpg.exceptions
|
||||||
import bleach
|
import bleach
|
||||||
from fastapi import APIRouter, File, UploadFile, HTTPException, Depends
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from classquiz.config import settings, redis, storage, meilisearch, ALLOWED_TAGS_FOR_QUIZ, server_regex
|
from classquiz.config import settings, redis, storage, meilisearch, ALLOWED_TAGS_FOR_QUIZ
|
||||||
from classquiz.db.models import Quiz, QuizInput, User, QuizQuestionType
|
from classquiz.db.models import Quiz, QuizInput, User, QuizQuestionType
|
||||||
import puremagic
|
|
||||||
from classquiz.auth import get_current_user
|
from classquiz.auth import get_current_user
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from classquiz.helpers import get_meili_data, check_hashcash
|
from classquiz.helpers import get_meili_data, check_image_string
|
||||||
from classquiz.storage.errors import DeletionFailedError
|
from classquiz.storage.errors import DeletionFailedError
|
||||||
|
|
||||||
settings = settings()
|
settings = settings()
|
||||||
@@ -86,37 +84,6 @@ class UploadImageReturn(BaseModel):
|
|||||||
pow_data: str
|
pow_data: str
|
||||||
|
|
||||||
|
|
||||||
@router.post("/image", response_model=UploadImageReturn)
|
|
||||||
async def upload_image(edit_id: str, pow_data: str, file: UploadFile = File()):
|
|
||||||
session_data = await redis.get(f"edit_session:{edit_id}")
|
|
||||||
pow_data_server = await redis.get(f"edit_session:{edit_id}:pow")
|
|
||||||
uploaded_images = await redis.llen(f"edit_session:{edit_id}:images")
|
|
||||||
if pow_data_server is None:
|
|
||||||
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
|
||||||
if session_data is None:
|
|
||||||
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
|
||||||
if uploaded_images == 0 and not check_hashcash(pow_data, pow_data_server, "8"):
|
|
||||||
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
|
||||||
if uploaded_images != 0 and not check_hashcash(pow_data, pow_data_server, "8"):
|
|
||||||
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
|
||||||
file_bytes = await file.read()
|
|
||||||
if len(file_bytes) > 2000000:
|
|
||||||
raise HTTPException(status_code=400, detail="File too large")
|
|
||||||
try:
|
|
||||||
pm_data = puremagic.magic_string(file_bytes)[0]
|
|
||||||
except puremagic.PureError:
|
|
||||||
raise HTTPException(status_code=400, detail="Image couldn't be identified!")
|
|
||||||
if pm_data.extension not in allowed_image_extensions:
|
|
||||||
raise HTTPException(status_code=400, detail="Image-type now allowed!")
|
|
||||||
session_data = EditSessionData.parse_raw(session_data)
|
|
||||||
file_name = f"{session_data.quiz_id}--{uuid.uuid4()}"
|
|
||||||
await storage.upload(file_name=file_name, file_data=file_bytes)
|
|
||||||
await redis.lpush(f"edit_session:{edit_id}:images", file_name)
|
|
||||||
random_str = os.urandom(8).hex()
|
|
||||||
await redis.set(f"edit_session:{edit_id}:pow", random_str, ex=3800)
|
|
||||||
return UploadImageReturn(id=file_name, pow_data=random_str)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/finish")
|
@router.post("/finish")
|
||||||
async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
||||||
session_data = await redis.get(f"edit_session:{edit_id}")
|
session_data = await redis.get(f"edit_session:{edit_id}")
|
||||||
@@ -141,10 +108,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
|||||||
quiz_input.questions[i].answers[i2].answer = html.unescape(
|
quiz_input.questions[i].answers[i2].answer = html.unescape(
|
||||||
bleach.clean(answer.answer, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True)
|
bleach.clean(answer.answer, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True)
|
||||||
)
|
)
|
||||||
image_id_regex = r"^.{36}--.{36}$"
|
|
||||||
imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$"
|
|
||||||
|
|
||||||
extract_file_name_re = r"^.*/api/v1/storage/download/(.{36}--.{36})$"
|
|
||||||
images_to_delete = []
|
images_to_delete = []
|
||||||
old_quiz_data: Quiz = await Quiz.objects.get_or_none(id=session_data.quiz_id, user_id=session_data.user_id)
|
old_quiz_data: Quiz = await Quiz.objects.get_or_none(id=session_data.quiz_id, user_id=session_data.user_id)
|
||||||
|
|
||||||
@@ -167,15 +131,9 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
|||||||
)
|
)
|
||||||
if image == "":
|
if image == "":
|
||||||
question.image = None
|
question.image = None
|
||||||
|
if image is None:
|
||||||
mark_image_for_deletion(question.image, i, old_quiz_data)
|
mark_image_for_deletion(question.image, i, old_quiz_data)
|
||||||
elif image is None:
|
elif check_image_string(quiz_input.cover_image)[0]:
|
||||||
mark_image_for_deletion(question.image, i, old_quiz_data)
|
|
||||||
elif bool(re.match(image_id_regex, question.image)):
|
|
||||||
question.image = f"{settings.root_address}/api/v1/storage/download/{image}"
|
|
||||||
mark_image_for_deletion(question.image, i, old_quiz_data)
|
|
||||||
elif bool(re.match(imgur_regex, image)):
|
|
||||||
mark_image_for_deletion(question.image, i, old_quiz_data)
|
|
||||||
elif bool(re.match(server_regex, image)):
|
|
||||||
mark_image_for_deletion(question.image, i, old_quiz_data)
|
mark_image_for_deletion(question.image, i, old_quiz_data)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=400, detail="Image URL(s) aren't valid!")
|
raise HTTPException(status_code=400, detail="Image URL(s) aren't valid!")
|
||||||
@@ -186,7 +144,10 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
|||||||
# if quiz_input.background_image is None and old_quiz_data.background_image is not None:
|
# if quiz_input.background_image is None and old_quiz_data.background_image is not None:
|
||||||
# mark_image_for_deletion(quiz_input.background_image)
|
# mark_image_for_deletion(quiz_input.background_image)
|
||||||
|
|
||||||
if quiz_input.cover_image is not None and not bool(re.match(server_regex, quiz_input.cover_image)):
|
if quiz_input.cover_image is not None and not check_image_string(quiz_input.cover_image)[0]:
|
||||||
|
raise HTTPException(status_code=400, detail="image url is not valid")
|
||||||
|
|
||||||
|
if quiz_input.background_image is not None and not check_image_string(quiz_input.background_image)[0]:
|
||||||
raise HTTPException(status_code=400, detail="image url is not valid")
|
raise HTTPException(status_code=400, detail="image url is not valid")
|
||||||
|
|
||||||
if session_data.edit:
|
if session_data.edit:
|
||||||
@@ -207,7 +168,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
|||||||
for image in images_to_delete:
|
for image in images_to_delete:
|
||||||
if image is not None:
|
if image is not None:
|
||||||
try:
|
try:
|
||||||
await storage.delete([re.search(extract_file_name_re, image).group(1)])
|
await storage.delete([image])
|
||||||
except DeletionFailedError:
|
except DeletionFailedError:
|
||||||
pass
|
pass
|
||||||
await redis.srem("edit_sessions", edit_id)
|
await redis.srem("edit_sessions", edit_id)
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ from fastapi.responses import StreamingResponse, RedirectResponse
|
|||||||
|
|
||||||
from classquiz.auth import get_current_user
|
from classquiz.auth import get_current_user
|
||||||
from classquiz.config import settings, storage, arq
|
from classquiz.config import settings, storage, arq
|
||||||
from classquiz.db.models import User, StorageItem
|
from classquiz.db.models import User, StorageItem, PublicStorageItem, UpdateStorageItem
|
||||||
from classquiz.storage.errors import DownloadingFailedError
|
from classquiz.storage.errors import DownloadingFailedError
|
||||||
from uuid import uuid4
|
from uuid import uuid4, UUID
|
||||||
|
|
||||||
settings = settings()
|
settings = settings()
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ async def download_file(file_name: str):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
async def upload_file(file: UploadFile = File(), user: User = Depends(get_current_user)):
|
async def upload_file(file: UploadFile = File(), user: User = Depends(get_current_user)) -> PublicStorageItem:
|
||||||
file_id = uuid4()
|
file_id = uuid4()
|
||||||
|
|
||||||
size = 0
|
size = 0
|
||||||
@@ -71,3 +71,39 @@ async def upload_file(file: UploadFile = File(), user: User = Depends(get_curren
|
|||||||
await storage.upload(file_name=file_id.hex, file_data=file_data)
|
await storage.upload(file_name=file_id.hex, file_data=file_data)
|
||||||
await file_obj.save()
|
await file_obj.save()
|
||||||
await arq.enqueue_job("calculate_hash", file_id.hex)
|
await arq.enqueue_job("calculate_hash", file_id.hex)
|
||||||
|
return PublicStorageItem.from_db_model(file_obj)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/meta/{file_id}")
|
||||||
|
async def get_file_info(file_id: UUID, user: User = Depends(get_current_user)) -> PublicStorageItem:
|
||||||
|
file_data = await StorageItem.objects.get_or_none(id=file_id, user=user, deleted_at=None)
|
||||||
|
if file_data is None:
|
||||||
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
return PublicStorageItem.from_db_model(file_data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/meta/{file_id}")
|
||||||
|
async def mark_file_as_deleted(file_id: UUID, user: User = Depends(get_current_user)):
|
||||||
|
file_data = await StorageItem.objects.get_or_none(id=file_id, user=user, deleted_at=None)
|
||||||
|
if file_data is None:
|
||||||
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
storage_path = file_data.storage_path
|
||||||
|
if storage_path is None:
|
||||||
|
storage_path = file_data.id.hex
|
||||||
|
await storage.delete(storage_path)
|
||||||
|
file_data.deleted_at = datetime.now()
|
||||||
|
await file_data.update()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/meta/{file_id}")
|
||||||
|
async def update_image_data(
|
||||||
|
file_id: UUID, data: UpdateStorageItem, user: User = Depends(get_current_user)
|
||||||
|
) -> PublicStorageItem:
|
||||||
|
file_data = await StorageItem.objects.get_or_none(id=file_id, user=user, deleted_at=None)
|
||||||
|
if file_data is None:
|
||||||
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
file_data.filename = data.filename
|
||||||
|
file_data.alt_text = data.alt_text
|
||||||
|
await file_data.update()
|
||||||
|
return PublicStorageItem.from_db_model(file_data)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from arq.worker import Retry
|
||||||
import xxhash
|
import xxhash
|
||||||
|
|
||||||
from classquiz.config import redis, storage
|
from classquiz.config import redis, storage
|
||||||
@@ -36,7 +37,11 @@ async def calculate_hash(ctx, file_id_as_str: str):
|
|||||||
if file_data.storage_path is not None:
|
if file_data.storage_path is not None:
|
||||||
file_path = file_data.storage_path
|
file_path = file_data.storage_path
|
||||||
file = SpooledTemporaryFile()
|
file = SpooledTemporaryFile()
|
||||||
file.write((await storage.download(file_path)).getbuffer().tobytes())
|
file_bytes = await storage.download(file_path)
|
||||||
|
if file_bytes is None:
|
||||||
|
print("Retry raised!")
|
||||||
|
raise Retry(defer=ctx["job_try"] * 10)
|
||||||
|
file.write(file_bytes.getbuffer().tobytes())
|
||||||
hash_obj = xxhash.xxh3_128()
|
hash_obj = xxhash.xxh3_128()
|
||||||
# assert hash_obj.block_size == 64
|
# assert hash_obj.block_size == 64
|
||||||
while chunk := file.read(6400):
|
while chunk := file.read(6400):
|
||||||
|
|||||||
@@ -225,7 +225,8 @@
|
|||||||
{#if quiz_data.questions[selected_question].image !== null}
|
{#if quiz_data.questions[selected_question].image !== null}
|
||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
src={quiz_data.questions[selected_question].image}
|
src="/api/v1/storage/download/{quiz_data.questions[selected_question]
|
||||||
|
.image}"
|
||||||
class="max-h-[20vh] object-cover mx-auto mb-8 w-auto"
|
class="max-h-[20vh] object-cover mx-auto mb-8 w-auto"
|
||||||
alt="Content for Question"
|
alt="Content for Question"
|
||||||
/>
|
/>
|
||||||
@@ -320,7 +321,7 @@
|
|||||||
<div class="h-[30vh] m-auto w-auto mt-12">
|
<div class="h-[30vh] m-auto w-auto mt-12">
|
||||||
<img
|
<img
|
||||||
class="max-h-full max-w-full block"
|
class="max-h-full max-w-full block"
|
||||||
src={quiz_data.cover_image}
|
src="/api/v1/storage/download/{quiz_data.cover_image}"
|
||||||
alt="Not provided"
|
alt="Not provided"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
<div class="h-[20vh] m-auto w-auto">
|
<div class="h-[20vh] m-auto w-auto">
|
||||||
<img
|
<img
|
||||||
class="max-h-full max-w-full block"
|
class="max-h-full max-w-full block"
|
||||||
src={quiz.cover_image}
|
src="/api/v1/storage/download/{quiz.cover_image}"
|
||||||
alt="Not provided"
|
alt="Not provided"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
@@ -302,7 +302,7 @@
|
|||||||
{#if visibleImages?.[i]?.[q]}
|
{#if visibleImages?.[i]?.[q]}
|
||||||
<img
|
<img
|
||||||
class="max-h-full max-w-full block"
|
class="max-h-full max-w-full block"
|
||||||
src={question.image}
|
src="/api/v1/storage/download/{question.image}"
|
||||||
alt="Not provided"
|
alt="Not provided"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -129,7 +129,8 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<img
|
<img
|
||||||
src={data.questions[selected_question].image}
|
src="/api/v1/storage/download/{data.questions[selected_question]
|
||||||
|
.image}"
|
||||||
alt="not available"
|
alt="not available"
|
||||||
class="max-h-64 h-auto w-auto"
|
class="max-h-64 h-auto w-auto"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<div
|
<div
|
||||||
class="dark:bg-gray-700 h-full"
|
class="dark:bg-gray-700 h-full"
|
||||||
style="background-repeat: no-repeat;background-size: 100% 100%;background-image: {data.background_image
|
style="background-repeat: no-repeat;background-size: 100% 100%;background-image: {data.background_image
|
||||||
? `url("${data.background_image}")`
|
? `url("/api/v1/storage/download/${data.background_image}")`
|
||||||
: `unset`}"
|
: `unset`}"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center pt-10 w-full">
|
<div class="flex justify-center pt-10 w-full">
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
{#if data.cover_image != undefined && data.cover_image !== ''}
|
{#if data.cover_image != undefined && data.cover_image !== ''}
|
||||||
<div class="flex justify-center pt-10 w-full max-h-72 w-full">
|
<div class="flex justify-center pt-10 w-full max-h-72 w-full">
|
||||||
<img
|
<img
|
||||||
src={data.cover_image}
|
src="/api/v1/storage/download/{data.cover_image}"
|
||||||
alt="not available"
|
alt="not available"
|
||||||
class="max-h-72 h-auto w-auto"
|
class="max-h-72 h-auto w-auto"
|
||||||
on:contextmenu|preventDefault={() => {
|
on:contextmenu|preventDefault={() => {
|
||||||
|
|||||||
@@ -201,11 +201,11 @@
|
|||||||
{#if question.image}
|
{#if question.image}
|
||||||
<div class="flex justify-center align-middle pb-0.5">
|
<div class="flex justify-center align-middle pb-0.5">
|
||||||
<img
|
<img
|
||||||
src={question.image}
|
src="/api/v1/storage/download/{question.image}"
|
||||||
class="h-10 border rounded-lg"
|
class="h-10 border rounded-lg"
|
||||||
alt="Not available"
|
alt="Not available"
|
||||||
use:tippy={{
|
use:tippy={{
|
||||||
content: `<img src='${question.image}' alt='Not available' class='rounded'>`,
|
content: `<img src='/api/v1/storage/download/${question.image}' alt='Not available' class='rounded'>`,
|
||||||
allowHTML: true
|
allowHTML: true
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -45,33 +45,29 @@
|
|||||||
quality: 0.6
|
quality: 0.6
|
||||||
})
|
})
|
||||||
.use(XHRUpload, {
|
.use(XHRUpload, {
|
||||||
endpoint: `/api/v1/editor/image?edit_id=${edit_id}&pow_data=${pow_data}`
|
endpoint: `/api/v1/storage/`
|
||||||
});
|
});
|
||||||
const props = {
|
const props = {
|
||||||
inline: true,
|
inline: true,
|
||||||
restrictions: {
|
restrictions: {
|
||||||
maxFileSize: 2_000_000,
|
maxFileSize: 10_000_000,
|
||||||
maxNumberOfFiles: 1,
|
maxNumberOfFiles: 1
|
||||||
allowedFileTypes: ['.gif', '.jpg', '.jpeg', '.png', '.svg', '.webp']
|
// allowedFileTypes: ['.gif', '.jpg', '.jpeg', '.png', '.svg', '.webp']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let image_id;
|
let image_id;
|
||||||
uppy.on('upload-success', (file, response) => {
|
uppy.on('upload-success', (file, response) => {
|
||||||
image_id = response.body.id;
|
image_id = response.body.id;
|
||||||
pow_salt = response.body.pow_data;
|
|
||||||
console.log(pow_salt, response.body);
|
|
||||||
pow_data = undefined;
|
pow_data = undefined;
|
||||||
});
|
});
|
||||||
uppy.on('complete', (_) => {
|
uppy.on('complete', (_) => {
|
||||||
console.log(pow_data);
|
console.log(pow_data);
|
||||||
if (selected_question === undefined) {
|
if (selected_question === undefined) {
|
||||||
data.cover_image = `${window.location.origin}/api/v1/storage/download/${image_id}`;
|
data.cover_image = image_id;
|
||||||
} else if (selected_question === -1) {
|
} else if (selected_question === -1) {
|
||||||
data.background_image = `${window.location.origin}/api/v1/storage/download/${image_id}`;
|
data.background_image = image_id;
|
||||||
} else {
|
} else {
|
||||||
data.questions[
|
data.questions[selected_question].image = image_id;
|
||||||
selected_question
|
|
||||||
].image = `${window.location.origin}/api/v1/storage/download/${image_id}`;
|
|
||||||
}
|
}
|
||||||
console.log(selected_question, data);
|
console.log(selected_question, data);
|
||||||
|
|
||||||
|
|||||||
@@ -384,5 +384,9 @@
|
|||||||
"popover": {
|
"popover": {
|
||||||
"copied_to_clipboard": "Copied to clipboard!"
|
"copied_to_clipboard": "Copied to clipboard!"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"public_user_page": {
|
||||||
|
"joined_on": "Joined on {{date}}",
|
||||||
|
"no_original_quizzes": "This user doesn't have any original quizzes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@
|
|||||||
{#if question.image !== null && game_mode !== 'kahoot'}
|
{#if question.image !== null && game_mode !== 'kahoot'}
|
||||||
<div class="max-h-full">
|
<div class="max-h-full">
|
||||||
<img
|
<img
|
||||||
src={question.image}
|
src="/api/v1/storage/download/{question.image}"
|
||||||
class="object-cover mx-auto mb-8 max-h-[90%]"
|
class="object-cover mx-auto mb-8 max-h-[90%]"
|
||||||
alt="Content for Question"
|
alt="Content for Question"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,7 +15,11 @@
|
|||||||
{#if cover_image}
|
{#if cover_image}
|
||||||
<div class="flex justify-center align-middle items-center">
|
<div class="flex justify-center align-middle items-center">
|
||||||
<div class="h-[30vh] m-auto w-auto mt-12">
|
<div class="h-[30vh] m-auto w-auto mt-12">
|
||||||
<img class="max-h-full max-w-full block" src={cover_image} alt="Not provided" />
|
<img
|
||||||
|
class="max-h-full max-w-full block"
|
||||||
|
src="/api/v1/storage/download/{cover_image}"
|
||||||
|
alt="Not provided"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
{#if question.image !== null}
|
{#if question.image !== null}
|
||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
src={question.image}
|
src="/api/v1/storage/download/{question.image}"
|
||||||
class="max-h-[40vh] object-cover mx-auto mb-8 w-auto"
|
class="max-h-[40vh] object-cover mx-auto mb-8 w-auto"
|
||||||
alt="Content for Question"
|
alt="Content for Question"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
{#if data.cover_image != undefined && data.cover_image !== ''}
|
{#if data.cover_image != undefined && data.cover_image !== ''}
|
||||||
<div class="flex justify-center pt-10 w-full max-h-72 w-full">
|
<div class="flex justify-center pt-10 w-full max-h-72 w-full">
|
||||||
<img
|
<img
|
||||||
src={data.cover_image}
|
src="/api/v1/storage/download/{data.cover_image}"
|
||||||
alt="not available"
|
alt="not available"
|
||||||
class="max-h-72 h-auto w-auto"
|
class="max-h-72 h-auto w-auto"
|
||||||
on:contextmenu|preventDefault={() => {
|
on:contextmenu|preventDefault={() => {
|
||||||
|
|||||||
@@ -65,14 +65,7 @@ export const dataSchema = yup.object({
|
|||||||
yup.object({
|
yup.object({
|
||||||
question: yup.string().required('A question-title is required').max(299),
|
question: yup.string().required('A question-title is required').max(299),
|
||||||
time: yup.number().required().positive('The time has to be positive'),
|
time: yup.number().required().positive('The time has to be positive'),
|
||||||
image: yup
|
image: yup.string().nullable().lowercase(),
|
||||||
.string()
|
|
||||||
.nullable()
|
|
||||||
.matches(
|
|
||||||
/^(http(|s):\/\/.*(|:)\d*\/api\/v1\/storage\/download\/.{36}--.{36}|https:\/\/i\.imgur\.com\/.{7}.(jpg|png|gif))$|^$/,
|
|
||||||
"The image-url isn't valid"
|
|
||||||
)
|
|
||||||
.lowercase(),
|
|
||||||
answers: yup.lazy((v) => {
|
answers: yup.lazy((v) => {
|
||||||
if (Array.isArray(v)) {
|
if (Array.isArray(v)) {
|
||||||
if (typeof v[0].right === 'boolean') {
|
if (typeof v[0].right === 'boolean') {
|
||||||
|
|||||||
@@ -180,7 +180,7 @@
|
|||||||
<div class="hidden lg:flex w-auto h-full items-center relative">
|
<div class="hidden lg:flex w-auto h-full items-center relative">
|
||||||
{#if quiz.cover_image}
|
{#if quiz.cover_image}
|
||||||
<img
|
<img
|
||||||
src={quiz.cover_image}
|
src="/api/v1/storage/download/{quiz.cover_image}"
|
||||||
alt="user provided"
|
alt="user provided"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="shrink-0 max-w-full max-h-full absolute"
|
class="shrink-0 max-w-full max-h-full absolute"
|
||||||
|
|||||||
@@ -253,7 +253,8 @@
|
|||||||
{#if game_data.questions[selected_question].image !== null}
|
{#if game_data.questions[selected_question].image !== null}
|
||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
src={game_data.questions[selected_question].image}
|
src="/api/v1/storage/download/{game_data.questions[selected_question]
|
||||||
|
.image}"
|
||||||
class="max-h-[20vh] object-cover mx-auto mb-8 w-auto"
|
class="max-h-[20vh] object-cover mx-auto mb-8 w-auto"
|
||||||
alt="Content for Question"
|
alt="Content for Question"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
<div class="h-[20vh] m-auto w-auto max-h-[18vh]">
|
<div class="h-[20vh] m-auto w-auto max-h-[18vh]">
|
||||||
<img
|
<img
|
||||||
class="max-h-full max-w-full block"
|
class="max-h-full max-w-full block"
|
||||||
src={quiz.cover_image}
|
src="/api/v1/storage/download/{quiz.cover_image}"
|
||||||
alt="Not provided"
|
alt="Not provided"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
<div class="h-[15vh] m-auto w-auto my-3">
|
<div class="h-[15vh] m-auto w-auto my-3">
|
||||||
<img
|
<img
|
||||||
class="max-h-full max-w-full block"
|
class="max-h-full max-w-full block"
|
||||||
src={quiz.cover_image}
|
src="/api/v1/storage/download/{quiz.cover_image}"
|
||||||
alt="Not provided"
|
alt="Not provided"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,7 +189,11 @@
|
|||||||
<!-- </label>-->
|
<!-- </label>-->
|
||||||
{#if question.image}
|
{#if question.image}
|
||||||
<span>
|
<span>
|
||||||
<img class="pl-8" src={question.image} alt="Not provided" />
|
<img
|
||||||
|
class="pl-8"
|
||||||
|
src="/api/v1/storage/download/{question.image}"
|
||||||
|
alt="Not provided"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<p
|
<p
|
||||||
|
|||||||
@@ -31,15 +31,19 @@ def upgrade() -> None:
|
|||||||
session = Session(bind=conn)
|
session = Session(bind=conn)
|
||||||
all_cover_images = session.execute("SELECT cover_image, id from quiz where cover_image is not null;")
|
all_cover_images = session.execute("SELECT cover_image, id from quiz where cover_image is not null;")
|
||||||
for cover_image, id in all_cover_images:
|
for cover_image, id in all_cover_images:
|
||||||
|
try:
|
||||||
new_cover_image = re.search(magic_regex, cover_image).group(1)
|
new_cover_image = re.search(magic_regex, cover_image).group(1)
|
||||||
# print(new_cover_image, id)
|
|
||||||
session.execute(f"UPDATE quiz SET cover_image = '{new_cover_image}' WHERE id='{id}';")
|
session.execute(f"UPDATE quiz SET cover_image = '{new_cover_image}' WHERE id='{id}';")
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
|
||||||
all_background_images = session.execute("SELECT background_image, id from quiz where background_image is not null;")
|
all_background_images = session.execute("SELECT background_image, id from quiz where background_image is not null;")
|
||||||
for bg_image, id in all_background_images:
|
for bg_image, id in all_background_images:
|
||||||
|
try:
|
||||||
new_bg_image = re.search(magic_regex, bg_image).group(1)
|
new_bg_image = re.search(magic_regex, bg_image).group(1)
|
||||||
# print(new_cover_image, id)
|
|
||||||
session.execute(f"UPDATE quiz SET cover_image = '{new_bg_image}' WHERE id='{id}';")
|
session.execute(f"UPDATE quiz SET cover_image = '{new_bg_image}' WHERE id='{id}';")
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
|
||||||
all_questions = session.execute("SELECT questions, id from quiz;")
|
all_questions = session.execute("SELECT questions, id from quiz;")
|
||||||
question_image_regex = rf"{settings.root_address}/api/v1/storage/download/(?=.{{36}}--.{{36}})"
|
question_image_regex = rf"{settings.root_address}/api/v1/storage/download/(?=.{{36}}--.{{36}})"
|
||||||
@@ -71,5 +75,5 @@ def downgrade() -> None:
|
|||||||
question_image_regex = r"(?=.{36}--.{36})"
|
question_image_regex = r"(?=.{36}--.{36})"
|
||||||
for question, id in all_questions:
|
for question, id in all_questions:
|
||||||
question_as_json = json.dumps(question)
|
question_as_json = json.dumps(question)
|
||||||
result = re.sub(question_image_regex, f"{settings.root_address}/api/v1/storage/download", question_as_json)
|
result = re.sub(question_image_regex, f"{settings.root_address}/api/v1/storage/download/", question_as_json)
|
||||||
session.execute(f"UPDATE quiz SET questions = '{result}' WHERE id='{id}';")
|
session.execute(f"UPDATE quiz SET questions = '{result}' WHERE id='{id}';")
|
||||||
|
|||||||
Reference in New Issue
Block a user