✨ Added local-file-system storage
This commit is contained in:
@@ -21,6 +21,7 @@ qrcode = "*"
|
||||
jinja2 = "*"
|
||||
argon2-cffi = "*"
|
||||
sentry-sdk = "*"
|
||||
aiofiles = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
||||
Generated
+12
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "0411374cb38f6ed3199ec87e5d1717b598f8af20c46cef66377af0c719c4b988"
|
||||
"sha256": "99a926c60fbb456ac477587d91947b39dfce66d39d4a2b6a35d1c41c04b4e32f"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -16,6 +16,14 @@
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"aiofiles": {
|
||||
"hashes": [
|
||||
"sha256:7a973fc22b29e9962d0897805ace5856e6a566ab1f0c8e5c91ff6c866519c937",
|
||||
"sha256:8334f23235248a3b2e83b2c3a78a22674f39969b96397126cc93664d9a901e59"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.8.0"
|
||||
},
|
||||
"aiohttp": {
|
||||
"hashes": [
|
||||
"sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3",
|
||||
@@ -291,7 +299,7 @@
|
||||
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
|
||||
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
|
||||
],
|
||||
"markers": "python_full_version >= '3.5.0'",
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.0.12"
|
||||
},
|
||||
"click": {
|
||||
@@ -536,7 +544,7 @@
|
||||
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||
],
|
||||
"markers": "python_full_version >= '3.5.0'",
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.3"
|
||||
},
|
||||
"jinja2": {
|
||||
@@ -957,7 +965,7 @@
|
||||
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
|
||||
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
|
||||
],
|
||||
"markers": "python_full_version >= '3.5.0'",
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
|
||||
+5
-2
@@ -26,8 +26,11 @@ class Settings(BaseSettings):
|
||||
# storage_backend
|
||||
storage_backend: str | None = "deta"
|
||||
# if storage_backend == "deta":
|
||||
deta_project_key: str
|
||||
deta_project_id: str
|
||||
deta_project_key: str | None
|
||||
deta_project_id: str | None
|
||||
|
||||
# if storage_backend == "local":
|
||||
storage_path: str | None
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
@@ -66,7 +66,7 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
|
||||
quiz_questions: list[dict] = []
|
||||
quiz_id = uuid.uuid4()
|
||||
storage = Storage(backend=settings.storage_backend, deta_key=settings.deta_project_key,
|
||||
deta_id=settings.deta_project_id)
|
||||
deta_id=settings.deta_project_id, storage_path=settings.storage_path)
|
||||
|
||||
for q in quiz.kahoot.questions:
|
||||
answers: list[QuizAnswer] = []
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from classquiz.config import settings
|
||||
import io
|
||||
import re
|
||||
from classquiz.storage import Storage
|
||||
|
||||
settings = settings()
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
file_regex = r"^[a-z0-9]{8}-[a-z0-9-]{27}--[a-z0-9-]{36}$"
|
||||
|
||||
|
||||
@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)
|
||||
deta_id=settings.deta_project_id, storage_path=settings.storage_path)
|
||||
download = await storage.download(file_name)
|
||||
if not re.match(file_regex, file_name):
|
||||
raise HTTPException(status_code=400, detail="Invalid file name")
|
||||
|
||||
def iter_file():
|
||||
yield from download
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
from .deta_storage import DetaStorage
|
||||
from .local_storage import LocalStorage
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class Storage:
|
||||
def __init__(self, backend: str, deta_key: str | None, deta_id: str | None):
|
||||
def __init__(self, backend: str, deta_key: str | None, deta_id: str | None, storage_path: 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)
|
||||
self.local_instance = LocalStorage(base_path=storage_path)
|
||||
if backend == "deta":
|
||||
if deta_key is None or deta_id is None:
|
||||
raise ValueError("deta_key and deta_id must be provided")
|
||||
if backend == "local":
|
||||
if storage_path is None:
|
||||
raise ValueError("storage_path must be provided")
|
||||
else:
|
||||
raise NotImplementedError(f"Backend {backend} not implemented")
|
||||
|
||||
@@ -19,7 +24,11 @@ class Storage:
|
||||
if self.backend == "deta":
|
||||
return await self.deta_instance.download(
|
||||
file_name)
|
||||
elif self.backend == "local":
|
||||
return await self.local_instance.get_file(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)
|
||||
elif self.backend == "local":
|
||||
return await self.local_instance.write_file(file_name=file_name, data=file_data)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import io
|
||||
import os
|
||||
import aiofiles
|
||||
|
||||
|
||||
class LocalStorage:
|
||||
def __init__(self, base_path: str):
|
||||
self.base_path = base_path
|
||||
|
||||
async def get_file(self, file_name: str) -> io.BytesIO:
|
||||
async with aiofiles.open(file=os.path.join(self.base_path, file_name), mode='rb') as f:
|
||||
return io.BytesIO(await f.read())
|
||||
|
||||
async def write_file(self, file_name: str, data: bytes) -> None:
|
||||
async with aiofiles.open(file=os.path.join(self.base_path, file_name), mode='wb') as f:
|
||||
await f.write(data)
|
||||
|
||||
@@ -22,10 +22,6 @@
|
||||
<p>Since ClassQuiz is open-source, it can also be self-hosted.</p>
|
||||
|
||||
|
||||
<h2>Warning</h2>
|
||||
<p>This "warning" is just temporary, because you need a <a href='https://deta.sh'>Deta</a> account (which is free)
|
||||
to store and serve the images getting imported with the kahoot-import function. I am planning to add more backends like s3 or the local file system. Untill then, Deta is needed. </p>
|
||||
|
||||
<h2>Requirements</h2>
|
||||
<ul>
|
||||
<li><a href='https://docker.com'>Docker</a></li>
|
||||
@@ -51,6 +47,21 @@
|
||||
</p>
|
||||
|
||||
<h2>Configuration</h2>
|
||||
<h3>Storage Provider</h3>
|
||||
<p>
|
||||
You'll have to set up a storage provider for some pictures (these getting imported from KAHOOT!). For now, you
|
||||
can use <a href='https://deta.sh'>Deta</a> or the local filesystem. Please note that I would <b>NOT</b> use it
|
||||
because of these funny path-things. I tried to prevent these attacks, but i really wouldn't trust it.
|
||||
You'll have to set the <code>STORAGE_BACKEND</code>-environment-variable to either <code>deta</code> or <code>local</code>.
|
||||
</p>
|
||||
<h4>If you chose Deta...</h4>
|
||||
<p>
|
||||
...you'll also have to set the <code>DETA_PROJECT_KEY</code> and the <code>DETA_PROJECT_ID</code>.
|
||||
</p>
|
||||
<h4>If you chose the local filesystem...</h4>
|
||||
<p>
|
||||
...you'll have to set the <code>STORAGE_PATH</code> enviromnent variable. The path must be absolute (so start with a <code>/</code>).
|
||||
</p>
|
||||
<p>
|
||||
Before you can start your stack, you have to set some environment-variables in your
|
||||
<code>docker-compose.yml</code>.
|
||||
@@ -92,6 +103,14 @@ services:
|
||||
SECRET_KEY: "ghfvfgjgvjgvbh" # openssl rand -hex 32
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: 30
|
||||
HCAPTCHA_KEY: "" # Private hCaptcha key for verification
|
||||
STORAGE_BACKEND: "deta" # MUST BE EITHER "deta" OR "local"
|
||||
|
||||
# If STORAGE_BACKEND is "deta"
|
||||
DETA_PROJECT_KEY: "YOUR_DETA_PROJECT_KEY"
|
||||
DETA_PROJECT_ID: "YOUR_DETA_PROJECT_ID"
|
||||
|
||||
# If STORAGE_BACKEND is "local"
|
||||
STORAGE_PATH: "/var/storage"
|
||||
redis:
|
||||
image: redis:alpine
|
||||
restart: always
|
||||
|
||||
Reference in New Issue
Block a user