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:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
],
"markers": "python_version >= '3.5'",
"markers": "python_full_version >= '3.5.0'",
"version": "==2.0.12"
},
"click": {
"hashes": [
"sha256:19a4baa64da924c5e0cd889aba8e947f280309f1a2ce0947a3e3a7bcb7cc72d6",
"sha256:977c213473c7665d3aa092b41ff12063227751c41d7b17165013e10069cc5cd2"
"sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e",
"sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.0"
"version": "==8.1.2"
},
"databases": {
"hashes": [
@@ -536,7 +536,7 @@
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3.5'",
"markers": "python_full_version >= '3.5.0'",
"version": "==3.3"
},
"jinja2": {
@@ -914,11 +914,11 @@
},
"redis": {
"hashes": [
"sha256:3cbe235cea80b9c9991b397567aa2d65eb4e6fb09787f61d227ae82eb4eb50b4",
"sha256:6758d01dec81af191b98a35cce3402675d115456584c39b500ab485a5e386bbb"
"sha256:69d05fac17bf3f43937afbb775c536eb516bd21355a4f17d59a966f4a531ce71",
"sha256:fe45513881229dbee610620b9e0817b1f48c47ba635870320fd44a712204bbdd"
],
"index": "pypi",
"version": "==4.2.0"
"version": "==4.2.1"
},
"rsa": {
"hashes": [
@@ -938,11 +938,11 @@
},
"setuptools": {
"hashes": [
"sha256:8f4813dd6a4d6cc17bde85fb2e635fe19763f96efbb0ddf5575562e5ee0bc47a",
"sha256:c3d4e2ab578fbf83775755cd76dae73627915a22832cf4ea5de895978767833b"
"sha256:425ec0e0014c5bcc1104dd1099de6c8f0584854fc9a4f512575f5ed5ee399fb9",
"sha256:6d59c30ce22dd583b42cacf51eebe4c6ea72febaa648aa8b30e5015d23a191fe"
],
"markers": "python_version >= '3.7'",
"version": "==61.2.0"
"version": "==61.3.0"
},
"six": {
"hashes": [
@@ -957,7 +957,7 @@
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
],
"markers": "python_version >= '3.5'",
"markers": "python_full_version >= '3.5.0'",
"version": "==1.2.0"
},
"sqlalchemy": {
@@ -1060,10 +1060,10 @@
},
"watchgod": {
"hashes": [
"sha256:4ba20c2fa3e63df706ab50e694b9453b05395fadb7cbbfd984d71fb1547d485d",
"sha256:c12d15f3df7d11e740704e45398277f75f1d78f46ad59ca9d7505bfd8b8d3086"
"sha256:2f3e8137d98f493ff58af54ea00f4d1433a6afe2ed08ab331a657df468c6bfce",
"sha256:cb11ff66657befba94d828e3b622d5fb76f22fbda1376f355f3e6e51e97d9450"
],
"version": "==0.8.1"
"version": "==0.8.2"
},
"websockets": {
"hashes": [
+2 -1
View File
@@ -4,7 +4,7 @@ from socketio import ASGIApp
from classquiz.db import database
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 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(utils.router, tags=["utils"], prefix="/api/v1/utils")
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))
+7 -1
View File
@@ -18,11 +18,17 @@ class Settings(BaseSettings):
mail_server: str
mail_port: int
secret_key: str
imgur_client_id: str = "b13fefc8bb1db87"
imgur_client_id: str = "27aa1687703ddb9"
access_token_expire_minutes: int = 30
cache_expiry: int = 86400
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:
env_file = ".env"
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.kahoot_importer.get import get as get_quiz
from classquiz.config import settings
from classquiz.storage import Storage
settings = settings()
async def _download_image(url: str) -> bytes:
async with ClientSession() as session:
async with session.get(url) as resp:
return await resp.read()
async def _upload_to_imgur(image: bytes, album_id: str | None) -> str:
"""
Uploads an image to Imgur.
:param image: The image to upload.
:return: The URL of the image on Imgur.
"""
formdata = FormData()
formdata.add_field('image', image, filename='image.png')
if album_id:
formdata.add_field('album', album_id)
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:
data = await resp.json()
return data["data"]["link"]
async def _generate_imgur_album(quiz_id: str) -> str:
"""
Generates an album on Imgur.
:param quiz_id: The ID of the quiz.
:return: The URL of the album on Imgur.
"""
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 _upload_to_imgur(image: bytes, album_id: str | None) -> str:
# """
# Uploads an image to Imgur.
# :param image: The image to upload.
# :return: The URL of the image on Imgur.
# """
#
# formdata = FormData()
# formdata.add_field('image', image, filename='image.png')
# if album_id:
# formdata.add_field('album', album_id)
# async with ClientSession(headers={"Authorization": f"Client-ID {settings.imgur_client_id}"}) as session:
# print(settings.imgur_client_id)
# async with session.post("https://api.imgur.com/3/image", data=image) as resp:
# data = await resp.json()
# print(resp.headers)
# try:
# return data["data"]["link"]
# except KeyError:
# raise Exception(data)
#
#
# async def _generate_imgur_album(quiz_id: str) -> str:
# # """
# # Generates an album on Imgur.
# # :param quiz_id: The ID of the quiz.
# # :return: The URL of the album on Imgur.
# # """
# 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:
"""
@@ -57,20 +64,24 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
if quiz is None:
return "quiz not found"
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:
answers: list[QuizAnswer] = []
image = None
if q.image is not None and 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
for a in q.choices:
answers.append((QuizAnswer(right=a.correct, answer=html.unescape(a.answer))))
quiz_questions.append(
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,
questions=json.dumps(quiz_questions))
return await quiz_data.save()
+4 -2
View File
@@ -9,8 +9,9 @@ from fastapi.responses import JSONResponse
import re
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
settings = settings()
router = APIRouter()
@@ -18,9 +19,10 @@ router = APIRouter()
@router.post("/create")
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)$"
server_regex = rf"^{settings.root_address}/api/v1/storage/download/.{36}--.{36}$"
for question in quiz_input.questions:
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")
quiz = Quiz(**quiz_input.dict(), user_id=user.id, id=uuid.uuid4())
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
async def join_game(sid, data):
async with aiohttp.ClientSession() as session:
async with session.post("https://hcaptcha.com/siteverify",
data={"response": data["captcha"], "secret": settings.hcaptcha_key}) as resp:
resp_data = await resp.json()
if not resp_data["success"]:
print("CAPTCHA FAILED")
return
try:
async with session.post("https://hcaptcha.com/siteverify",
data={"response": data["captcha"], "secret": settings.hcaptcha_key}) as resp:
resp_data = await resp.json()
if not resp_data["success"]:
print("CAPTCHA FAILED")
return
except KeyError as e:
print("CAPTCHA FAILED")
return
redis_res = await redis.get(f"game:{data['game_pin']}")
if redis_res is None:
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>) => {
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++) {
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;
}
}
+5 -1
View File
@@ -50,7 +50,11 @@
</div>
{#if question.image !== null}
<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>
{/if}
{#if timer_res !== '0'}