Migrated to deta from imgur for image-storing

This commit is contained in:
Mawoka
2022-04-01 18:38:32 +02:00
parent 6d28608824
commit b3a54ce4f5
11 changed files with 182 additions and 62 deletions
Generated
+15 -15
View File
@@ -291,16 +291,16 @@
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
], ],
"markers": "python_version >= '3.5'", "markers": "python_full_version >= '3.5.0'",
"version": "==2.0.12" "version": "==2.0.12"
}, },
"click": { "click": {
"hashes": [ "hashes": [
"sha256:19a4baa64da924c5e0cd889aba8e947f280309f1a2ce0947a3e3a7bcb7cc72d6", "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e",
"sha256:977c213473c7665d3aa092b41ff12063227751c41d7b17165013e10069cc5cd2" "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==8.1.0" "version": "==8.1.2"
}, },
"databases": { "databases": {
"hashes": [ "hashes": [
@@ -536,7 +536,7 @@
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
], ],
"markers": "python_version >= '3.5'", "markers": "python_full_version >= '3.5.0'",
"version": "==3.3" "version": "==3.3"
}, },
"jinja2": { "jinja2": {
@@ -914,11 +914,11 @@
}, },
"redis": { "redis": {
"hashes": [ "hashes": [
"sha256:3cbe235cea80b9c9991b397567aa2d65eb4e6fb09787f61d227ae82eb4eb50b4", "sha256:69d05fac17bf3f43937afbb775c536eb516bd21355a4f17d59a966f4a531ce71",
"sha256:6758d01dec81af191b98a35cce3402675d115456584c39b500ab485a5e386bbb" "sha256:fe45513881229dbee610620b9e0817b1f48c47ba635870320fd44a712204bbdd"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.2.0" "version": "==4.2.1"
}, },
"rsa": { "rsa": {
"hashes": [ "hashes": [
@@ -938,11 +938,11 @@
}, },
"setuptools": { "setuptools": {
"hashes": [ "hashes": [
"sha256:8f4813dd6a4d6cc17bde85fb2e635fe19763f96efbb0ddf5575562e5ee0bc47a", "sha256:425ec0e0014c5bcc1104dd1099de6c8f0584854fc9a4f512575f5ed5ee399fb9",
"sha256:c3d4e2ab578fbf83775755cd76dae73627915a22832cf4ea5de895978767833b" "sha256:6d59c30ce22dd583b42cacf51eebe4c6ea72febaa648aa8b30e5015d23a191fe"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==61.2.0" "version": "==61.3.0"
}, },
"six": { "six": {
"hashes": [ "hashes": [
@@ -957,7 +957,7 @@
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
], ],
"markers": "python_version >= '3.5'", "markers": "python_full_version >= '3.5.0'",
"version": "==1.2.0" "version": "==1.2.0"
}, },
"sqlalchemy": { "sqlalchemy": {
@@ -1060,10 +1060,10 @@
}, },
"watchgod": { "watchgod": {
"hashes": [ "hashes": [
"sha256:4ba20c2fa3e63df706ab50e694b9453b05395fadb7cbbfd984d71fb1547d485d", "sha256:2f3e8137d98f493ff58af54ea00f4d1433a6afe2ed08ab331a657df468c6bfce",
"sha256:c12d15f3df7d11e740704e45398277f75f1d78f46ad59ca9d7505bfd8b8d3086" "sha256:cb11ff66657befba94d828e3b622d5fb76f22fbda1376f355f3e6e51e97d9450"
], ],
"version": "==0.8.1" "version": "==0.8.2"
}, },
"websockets": { "websockets": {
"hashes": [ "hashes": [
+2 -1
View File
@@ -4,7 +4,7 @@ from socketio import ASGIApp
from classquiz.db import database from classquiz.db import database
from classquiz.config import settings from classquiz.config import settings
from classquiz.routers import users, quiz, utils, stats from classquiz.routers import users, quiz, utils, stats, storage
from classquiz.socket_server import sio from classquiz.socket_server import sio
from sentry_sdk.integrations.redis import RedisIntegration from sentry_sdk.integrations.redis import RedisIntegration
@@ -50,4 +50,5 @@ app.include_router(users.router, tags=["users"], prefix="/api/v1/users")
app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz") app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz")
app.include_router(utils.router, tags=["utils"], prefix="/api/v1/utils") app.include_router(utils.router, tags=["utils"], prefix="/api/v1/utils")
app.include_router(stats.router, tags=["stats"], prefix="/api/v1/stats") app.include_router(stats.router, tags=["stats"], prefix="/api/v1/stats")
app.include_router(storage.router, tags=["storage"], prefix="/api/v1/storage")
app.mount("/", ASGIApp(sio)) app.mount("/", ASGIApp(sio))
+7 -1
View File
@@ -18,11 +18,17 @@ class Settings(BaseSettings):
mail_server: str mail_server: str
mail_port: int mail_port: int
secret_key: str secret_key: str
imgur_client_id: str = "b13fefc8bb1db87" imgur_client_id: str = "27aa1687703ddb9"
access_token_expire_minutes: int = 30 access_token_expire_minutes: int = 30
cache_expiry: int = 86400 cache_expiry: int = 86400
sentry_dsn: str | None = "https://4981de1f72f24fd7b5e21b8913b93a02@o661934.ingest.sentry.io/6254641" sentry_dsn: str | None = "https://4981de1f72f24fd7b5e21b8913b93a02@o661934.ingest.sentry.io/6254641"
# storage_backend
storage_backend: str | None = "deta"
# if storage_backend == "deta":
deta_project_key: str
deta_project_id: str
class Config: class Config:
env_file = ".env" env_file = ".env"
env_file_encoding = 'utf-8' env_file_encoding = 'utf-8'
+44 -33
View File
@@ -7,44 +7,51 @@ import html
from classquiz.db.models import Quiz, QuizAnswer, QuizQuestion, User from classquiz.db.models import Quiz, QuizAnswer, QuizQuestion, User
from classquiz.kahoot_importer.get import get as get_quiz from classquiz.kahoot_importer.get import get as get_quiz
from classquiz.config import settings from classquiz.config import settings
from classquiz.storage import Storage
settings = settings() settings = settings()
async def _download_image(url: str) -> bytes: async def _download_image(url: str) -> bytes:
async with ClientSession() as session: async with ClientSession() as session:
async with session.get(url) as resp: async with session.get(url) as resp:
return await resp.read() return await resp.read()
async def _upload_to_imgur(image: bytes, album_id: str | None) -> str: # async def _upload_to_imgur(image: bytes, album_id: str | None) -> str:
""" # """
Uploads an image to Imgur. # Uploads an image to Imgur.
:param image: The image to upload. # :param image: The image to upload.
:return: The URL of the image on Imgur. # :return: The URL of the image on Imgur.
""" # """
#
formdata = FormData() # formdata = FormData()
formdata.add_field('image', image, filename='image.png') # formdata.add_field('image', image, filename='image.png')
if album_id: # if album_id:
formdata.add_field('album', album_id) # formdata.add_field('album', album_id)
async with ClientSession(headers={"Authorization": f"Client-ID {settings.imgur_client_id}"}) as session: # async with ClientSession(headers={"Authorization": f"Client-ID {settings.imgur_client_id}"}) as session:
async with session.post("https://api.imgur.com/3/image", data=image) as resp: # print(settings.imgur_client_id)
data = await resp.json() # async with session.post("https://api.imgur.com/3/image", data=image) as resp:
return data["data"]["link"] # data = await resp.json()
# print(resp.headers)
# try:
async def _generate_imgur_album(quiz_id: str) -> str: # return data["data"]["link"]
""" # except KeyError:
Generates an album on Imgur. # raise Exception(data)
:param quiz_id: The ID of the quiz. #
:return: The URL of the album on Imgur. #
""" # async def _generate_imgur_album(quiz_id: str) -> str:
formdata = FormData() # # """
formdata.add_field('title', quiz_id) # # Generates an album on Imgur.
async with ClientSession(headers={"Authorization": f"Client-ID {settings.imgur_client_id}"}) as session: # # :param quiz_id: The ID of the quiz.
async with session.post("https://api.imgur.com/3/album", data={"title": "Quiz: " + quiz_id}, ) as resp: # # :return: The URL of the album on Imgur.
data = await resp.json() # # """
return data["data"]["id"] # formdata = FormData()
# formdata.add_field('title', quiz_id)
# async with ClientSession(headers={"Authorization": f"Client-ID {settings.imgur_client_id}"}) as session:
# async with session.post("https://api.imgur.com/3/album", data={"title": "Quiz: " + quiz_id}, ) as resp:
# data = await resp.json()
# return data["data"]["id"]
async def import_quiz(quiz_id: str, user: User) -> Quiz | str: async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
""" """
@@ -57,20 +64,24 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
if quiz is None: if quiz is None:
return "quiz not found" return "quiz not found"
quiz_questions: list[dict] = [] quiz_questions: list[dict] = []
imgur_album_id = await _generate_imgur_album(quiz_id) quiz_id = uuid.uuid4()
storage = Storage(backend=settings.storage_backend, deta_key=settings.deta_project_key,
deta_id=settings.deta_project_id)
for q in quiz.kahoot.questions: for q in quiz.kahoot.questions:
answers: list[QuizAnswer] = [] answers: list[QuizAnswer] = []
image = None image = None
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 = await _upload_to_imgur(image_bytes, imgur_album_id) image_name= f"{quiz_id}--{uuid.uuid4()}"
image = await storage.upload(file_name=image_name, file_data=image_bytes)
image = f"{settings.root_address}/api/v1/storage/download/{image_name}"
# image = q.image # image = q.image
for a in q.choices: for a in q.choices:
answers.append((QuizAnswer(right=a.correct, answer=html.unescape(a.answer)))) answers.append((QuizAnswer(right=a.correct, answer=html.unescape(a.answer))))
quiz_questions.append( quiz_questions.append(
QuizQuestion(question=q.question, answers=answers, time=str(q.time / 1000), image=image).dict()) QuizQuestion(question=q.question, answers=answers, time=str(q.time / 1000), image=image).dict())
quiz_data = Quiz(id=uuid.uuid4(), public=False, title=quiz.kahoot.title, description=quiz.kahoot.description, quiz_data = Quiz(id=quiz_id, public=False, title=quiz.kahoot.title, description=quiz.kahoot.description,
created_at=datetime.now(), updated_at=datetime.now(), user_id=user.id, created_at=datetime.now(), updated_at=datetime.now(), user_id=user.id,
questions=json.dumps(quiz_questions)) questions=json.dumps(quiz_questions))
return await quiz_data.save() return await quiz_data.save()
+4 -2
View File
@@ -9,8 +9,9 @@ from fastapi.responses import JSONResponse
import re import re
from classquiz.auth import get_current_user, get_current_user_optional from classquiz.auth import get_current_user, get_current_user_optional
from classquiz.config import redis from classquiz.config import redis, settings
from classquiz.db.models import Quiz, QuizInput, User, PlayGame from classquiz.db.models import Quiz, QuizInput, User, PlayGame
settings = settings()
router = APIRouter() router = APIRouter()
@@ -18,9 +19,10 @@ router = APIRouter()
@router.post("/create") @router.post("/create")
async def create_quiz_lol(quiz_input: QuizInput, user: User = Depends(get_current_user)): async def create_quiz_lol(quiz_input: QuizInput, user: User = Depends(get_current_user)):
imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$" imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$"
server_regex = rf"^{settings.root_address}/api/v1/storage/download/.{36}--.{36}$"
for question in quiz_input.questions: for question in quiz_input.questions:
if question.image is not None: if question.image is not None:
if not re.match(imgur_regex, question.image): if not re.match(imgur_regex, question.image) or not re.match(server_regex, question.image):
raise HTTPException(status_code=400, detail="image url is not valid") raise HTTPException(status_code=400, detail="image url is not valid")
quiz = Quiz(**quiz_input.dict(), user_id=user.id, id=uuid.uuid4()) quiz = Quiz(**quiz_input.dict(), user_id=user.id, id=uuid.uuid4())
await redis.delete("global_quiz_count") await redis.delete("global_quiz_count")
+21
View File
@@ -0,0 +1,21 @@
from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from classquiz.config import settings
import io
from classquiz.storage import Storage
settings = settings()
router = APIRouter()
@router.get('/download/{file_name}')
async def download_file(file_name: str):
storage = Storage(backend=settings.storage_backend, deta_key=settings.deta_project_key,
deta_id=settings.deta_project_id)
download = await storage.download(file_name)
def iter_file():
yield from download
return StreamingResponse(iter_file(), media_type='image/*')
+10 -6
View File
@@ -13,12 +13,16 @@ settings = settings()
@sio.event @sio.event
async def join_game(sid, data): async def join_game(sid, data):
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post("https://hcaptcha.com/siteverify", try:
data={"response": data["captcha"], "secret": settings.hcaptcha_key}) as resp: async with session.post("https://hcaptcha.com/siteverify",
resp_data = await resp.json() data={"response": data["captcha"], "secret": settings.hcaptcha_key}) as resp:
if not resp_data["success"]: resp_data = await resp.json()
print("CAPTCHA FAILED") if not resp_data["success"]:
return print("CAPTCHA FAILED")
return
except KeyError as e:
print("CAPTCHA FAILED")
return
redis_res = await redis.get(f"game:{data['game_pin']}") redis_res = await redis.get(f"game:{data['game_pin']}")
if redis_res is None: if redis_res is None:
await sio.emit("game_not_found", room=sid) await sio.emit("game_not_found", room=sid)
+25
View File
@@ -0,0 +1,25 @@
from .deta_storage import DetaStorage
from io import BytesIO
class Storage:
def __init__(self, backend: str, deta_key: str | None, deta_id: str | None):
self.backend = backend
self.deta_key = deta_key
self.deta_id = deta_id
self.deta_base_url = f"https://drive.deta.sh/v1/{deta_id}/classquiz"
self.deta_instance = DetaStorage(deta_base_url=self.deta_base_url, deta_key=self.deta_key, deta_id=self.deta_id)
if backend == "deta":
if deta_key is None or deta_id is None:
raise ValueError("deta_key and deta_id must be provided")
else:
raise NotImplementedError(f"Backend {backend} not implemented")
async def download(self, file_name: str) -> BytesIO | None:
if self.backend == "deta":
return await self.deta_instance.download(
file_name)
async def upload(self, file_name: str, file_data: bytes) -> None:
if self.backend == "deta":
return await self.deta_instance.upload(file=file_data, file_name=file_name)
+43
View File
@@ -0,0 +1,43 @@
from classquiz.config import settings
from aiohttp import ClientSession
from io import BytesIO
settings = settings()
class DetaStorage:
def __init__(self, deta_base_url: str, deta_id: str, deta_key: str):
self.deta_url = deta_base_url
self.deta_id = deta_id
self.deta_key = deta_key
self.headers = {
"X-Api-Key": self.deta_key,
}
async def download(self, file_name: str) -> BytesIO | None:
"""
:param file_name: The name of the file to be downloaded
:return: Either bytes f successfull download or None if failed
"""
async with ClientSession(headers=self.headers) as session:
async with session.get(f"{self.deta_url}/files/download?name={file_name}") as response:
if response.status == 200:
return BytesIO(await response.read())
elif response.status == 404:
return None
else:
raise Exception("Download failed")
async def upload(self, file: bytes, file_name: str) -> None:
"""
:param file: The file in bytes
:param file_name: The name of the file
:return:
"""
async with ClientSession(headers=self.headers) as session:
async with session.post(f"{self.deta_url}/files?name={file_name}", data=file) as response:
if response.status == 201:
return None
else:
raise Exception("Upload failed")
+6 -3
View File
@@ -40,11 +40,14 @@
const checkIfAllQuestionImagesComplyWithRegex = (questions: Array<Question>) => { const checkIfAllQuestionImagesComplyWithRegex = (questions: Array<Question>) => {
let NoteverythingValid = false; let NoteverythingValid = false;
const regex = /^https:\/\/i\.imgur\.com\/.{7}.(jpg|png|gif)$/; // const regex = /^https:\/\/i\.imgur\.com\/.{7}.(jpg|png|gif)$/;
// const local_regex = /^http(|s):\/\/\w*(|:)\d*\/api\/v1\/storage\/download\/.{36}--.{36}$/g;
const main_regex =
/^(http(|s):\/\/\w*(|:)\d*\/api\/v1\/storage\/download\/.{36}--.{36}|https:\/\/i\.imgur\.com\/.{7}.(jpg|png|gif))$/;
for (let i = 0; i < questions.length; i++) { for (let i = 0; i < questions.length; i++) {
const question = questions[i]; const question = questions[i];
if (question.image && !regex.test(question.image)) {
console.log('not valid'); if (question.image && !main_regex.test(question.image)) {
NoteverythingValid = true; NoteverythingValid = true;
} }
} }
+5 -1
View File
@@ -50,7 +50,11 @@
</div> </div>
{#if question.image !== null} {#if question.image !== null}
<div> <div>
<img src={question.image} class="w-full h-full object-cover" alt="Question image" /> <img
src={question.image}
class="h-2/5 object-cover mx-auto mb-8"
alt="Content for Question"
/>
</div> </div>
{/if} {/if}
{#if timer_res !== '0'} {#if timer_res !== '0'}