Added data download ability

This commit is contained in:
2023-02-19 21:10:00 +00:00
parent 48f080de7c
commit c850726f91
64 changed files with 12734 additions and 2 deletions

View File

@@ -7,5 +7,4 @@ COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]

7
backend/main.py Normal file
View File

@@ -0,0 +1,7 @@
import uvicorn
from src.app import app
if __name__ == "__main__":
# Run the app with reload and pass app as string
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

View File

@@ -1 +1,10 @@
fastapi
uvicorn
sqlmodel
python-dotenv
fastapi_jwt_auth
aiosqlite
passlib
requests
python-multipart
Pillow

0
backend/src/__init__.py Normal file
View File

115
backend/src/app.py Normal file
View File

@@ -0,0 +1,115 @@
import inspect
import re
from datetime import timedelta
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.utils import get_openapi
from fastapi.routing import APIRoute
from fastapi_jwt_auth import AuthJWT
from fastapi_jwt_auth.exceptions import AuthJWTException
from pydantic import BaseModel
import src.settings as settings
from src.database.database import init_db
from src.exceptions.base_exception import BaseException
from src.exceptions.exception_handlers import (auth_exception_handler,
base_exception_handler)
app = FastAPI(title="SignDataCollector", version="0.0.1")
@app.on_event("startup")
async def on_startup():
await init_db()
class AuthSettings(BaseModel):
"""AuthSettings model"""
authjwt_secret_key: str = settings.JWT_SECRET_KEY
# authjwt_denylist_enabled: bool = True
authjwt_denylist_enabled: bool = False
authjwt_denylist_token_checks: dict = {"access", "refresh"}
access_expires: timedelta = timedelta(seconds=settings.ACCESS_EXPIRES)
refresh_expires: timedelta = timedelta(seconds=settings.REFRESH_EXPIRES)
authjwt_cookie_csrf_protect: bool = True
authjwt_token_location: dict = {"headers"}
authjwt_cookie_samesite: str = "lax"
@AuthJWT.load_config
def auth_config():
return AuthSettings()
# Add middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# include the routers
from .routers import auth_router, signs_router, signvideo_router
app.include_router(auth_router)
app.include_router(signs_router)
app.include_router(signvideo_router)
# Add the exception handlers
app.add_exception_handler(BaseException, base_exception_handler)
app.add_exception_handler(AuthJWTException, auth_exception_handler)
def custom_openapi():
"""custom_openapi generate the custom swagger api documentation
:return: custom openapi_schema
:rtype: openapi_schema
"""
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title="My Auth API",
version="1.0",
description="An API with an Authorize Button",
routes=app.routes,
# servers=[{"url": config.api_path}],
)
openapi_schema["components"]["securitySchemes"] = {
"Bearer Auth": {
"type": "apiKey",
"in": "header",
"name": "Authorization",
"description": "Enter: **'Bearer <JWT>'**, where JWT is the access token",
}
}
# Get all routes where jwt_optional() or jwt_required
api_router = [route for route in app.routes if isinstance(route, APIRoute)]
for route in api_router:
path = getattr(route, "path")
endpoint = getattr(route, "endpoint")
methods = [method.lower() for method in getattr(route, "methods")]
for method in methods:
# access_token
if (
re.search("RoleChecker", inspect.getsource(endpoint))
or re.search("jwt_required", inspect.getsource(endpoint))
or re.search("fresh_jwt_required", inspect.getsource(endpoint))
or re.search("jwt_optional", inspect.getsource(endpoint))
):
openapi_schema["paths"][path][method]["security"] = [
{"Bearer Auth": []}
]
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi

View File

@@ -0,0 +1,129 @@
""" This module includes the create, read, update and delete functions for the models """
from typing import List, Optional, Type, TypeVar
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlmodel import SQLModel
T = TypeVar("T", SQLModel, object)
async def read_all_where(
model: T, *args, select_in_load: List = [], session: AsyncSession
) -> List[T]:
"""read_all_where this function reads all the entries from a specific model,
if a key and a value are passed this will be checked on each instance of model
example read all Users: read_all_where(User)
example read all Users with Role admin: read_all_where(User, User.role, UserRole.ADMIN)
:param model: the model to read all entries from
:type model: SQLModel
:param key: the key to check the value on, pass this argument as SQLModel.key (see the example)
:param value: the value that the key should have
:return: list with all data-entries of type model
:rtype: List[SQLModel]
"""
statement = select(model)
for arg in args:
statement = statement.where(arg)
for sil in select_in_load:
statement = statement.options(selectinload(sil))
res = await session.execute(statement)
return [value for (value,) in res.all()]
async def read_where(
model: T, *args, select_in_load: List = [], session: AsyncSession
) -> Optional[T]:
"""read_where this function reads one entry from a specific model that matches the given key and value
example read user with id user_id: read_where(User, User.id == user_id)
:param model: the model to read all entries from
:type model: SQLModel
:param key: the key to check the value on, pass this argument as SQLModel.key (see the example)
:param value: the value that the key should have
:return: a data-entry of type model that has values as values for the keys, or None is no such data-entry could be found
:rtype: Optional[SQLModel]
"""
statement = select(model)
for arg in args:
statement = statement.where(arg)
for sil in select_in_load:
statement = statement.options(selectinload(sil))
res = await session.execute(statement)
first = res.first()
if not first:
return None
return first[0]
async def count_where(model: Type[T], *args, session: AsyncSession) -> int:
"""count_where Count the objects where in mongodb
:param model: _description_
:type model: Type[SQLModel]
:return: the object count
:rtype: int
"""
statement = select(model)
for arg in args:
statement = statement.where(arg)
res = await session.execute(statement)
if res is None:
return 0
return len(res.all())
async def update(model: T, session: AsyncSession) -> Optional[T]:
"""update this function updates one entry from a model (the one with the same id, or else it adds the id)
example new_user is the updated version of the old user but the id remained: update(new_user)
:param model: an instance of the model to update / create
:type model: SQLModel
:return: the updated user upon succes
:rtype: Optional[SQLModel]
"""
session.add(model)
await session.commit()
await session.refresh(model)
return model
async def update_all(models: List[T], session: AsyncSession) -> Optional[List[T]]:
"""update this function updates all entries from models (the one with the same id, or else it adds the id)
example new_user is the updated version of the old user but the id remained: update(new_user)
:param models: a list of instances of models to update / create
:type models: List[SQLModel]
:return: the updated user upon success
:rtype: Optional[SQLModel]
"""
session.add_all(models)
await session.commit()
for model in models:
await session.refresh(model)
return models
async def delete(model: T, session: AsyncSession) -> None:
"""Deletes the given model from the database
:param model: an instance of the model to delete
:type model: SQLModel
:return: None
:rtype: None
"""
await session.delete(model)
await session.commit()

View File

@@ -0,0 +1,59 @@
""" This module includes the function for the database connection """
from sqlalchemy import text
from sqlalchemy.orm import sessionmaker
from sqlmodel import SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession
import src.settings as settings
from src.models.auth import User
from src.utils.cryptography import get_password_hash
async def init_db(fill=True):
from src.database.engine import engine
async with engine.begin() as conn:
# await conn.run_sync(SQLModel.metadata.drop_all)
if settings.TEST_MODE or settings.DB_USE_SQLITE:
await conn.execute(text("PRAGMA foreign_keys=ON"))
await conn.run_sync(SQLModel.metadata.create_all)
if settings.DEFAULT_USER_ENABLED:
async_session = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
async with async_session() as session:
# check if there exists a user
users = await User.get_all(session=session)
if len(users) == 0:
# create a new user
user = User(
email=settings.DEFAULT_USER_EMAIL,
hashed_password=get_password_hash(settings.DEFAULT_USER_PASSWORD),
)
await user.save(session=session)
if fill:
async_session = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
async with async_session() as session:
# await fill_database(session=session)
pass
async def get_session() -> AsyncSession:
"""get_session return a new session
:return: a new session
:rtype: AsyncSession
:yield: a new session
:rtype: Iterator[AsyncSession]
"""
from src.database.engine import engine
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
yield session

View File

@@ -0,0 +1,47 @@
import json
from urllib.parse import quote_plus
import pydantic
from sqlalchemy.engine import URL
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
import src.settings as settings
engine: AsyncEngine
def _custom_json_serializer(*args, **kwargs) -> str:
"""
Encodes json in the same way that pydantic does.
"""
return json.dumps(*args, default=pydantic.json.pydantic_encoder, **kwargs)
# check if the engine must be created for the tests -> in memory sqlite
if settings.TEST_MODE:
engine = create_async_engine(
"sqlite+aiosqlite://",
connect_args={"check_same_thread": False},
json_serializer=_custom_json_serializer,
)
elif settings.DB_USE_SQLITE:
engine = create_async_engine(
URL.create(drivername="sqlite+aiosqlite", database=settings.DB_SQLITE_PATH),
connect_args={"check_same_thread": False},
json_serializer=_custom_json_serializer,
)
else:
# use the postgresql database
_encoded_password = quote_plus(settings.DB_PASSWORD)
engine = create_async_engine(
URL.create(
drivername="postgresql+asyncpg",
username=settings.DB_USER,
password=_encoded_password,
host=settings.DB_HOST,
port=settings.DB_PORT,
database=settings.DB_NAME,
json_serializer=_custom_json_serializer,
),
pool_pre_ping=True,
)

View File

@@ -0,0 +1,47 @@
""" This module includes the BaseException
"""
import json
from fastapi.responses import JSONResponse
class BaseException(Exception):
"""BaseException is the base class for all exceptions
:param Exception: inherits from python Exception class
"""
def __init__(self, status_code: int, message: str):
"""__init__ init the class with the status code and message
:param status_code: the status code of the response
:type status_code: int
:param message: the message of exception
:type message: str
"""
self.status_code = status_code
self.message = message
def json(self) -> JSONResponse:
"""json return the json of the exception
:return: json reponse of exception
:rtype: JSONResponse
"""
return JSONResponse(
status_code=self.status_code, content={"message": self.message}
)
def checkResponse(self, response: BaseException) -> bool:
"""checkResponse compare the response exception with the exception
:param response: the response exception
:type response: _type_
:return: if excepiton equals the response exception
:rtype: bool
"""
return (
response.status_code == self.status_code
and json.loads(response.content)["message"] == self.message
)

View File

@@ -0,0 +1,31 @@
from fastapi import Request
from fastapi.responses import JSONResponse
from fastapi_jwt_auth.exceptions import AuthJWTException
async def base_exception_handler(request: Request, exception: BaseException):
"""my_exception_handler handler for the raised exceptions
:param request: the request
:type request: Request
:param exception: the raised exception
:type exception: BaseException
:return: the exception in json format
:rtype: JSONResponse
"""
return exception.json()
async def auth_exception_handler(request: Request, exception: AuthJWTException):
"""auth_exception_handler handler for the raised exceptions
:param request: the request
:type request: Request
:param exception: the raised exception
:type exception: AuthJWTException
:return: the exception in json format
:rtype: JSONResponse
"""
return JSONResponse(
status_code=exception.status_code, content={"message": exception.message}
)

View File

@@ -0,0 +1,11 @@
from src.exceptions.base_exception import BaseException
class LoginException(BaseException):
"""LoginException exception raised when something went wrong during the login process
:param BaseException: inherits from BaseException
"""
def __init__(self, message: str = ""):
"""__init__ inits parent class with status code and message"""
super().__init__(400, message)

View File

@@ -0,0 +1,53 @@
from typing import List, Optional
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import SQLModel
from src.database.crud import delete, read_all_where, read_where, update
class SQLModelExtended(SQLModel):
__abstract__ = True
@classmethod
async def get_all(
self, session: AsyncSession, select_in_load: List = []
) -> List[SQLModel]:
res = await read_all_where(self, select_in_load=select_in_load, session=session)
return res
async def save(self, session: AsyncSession):
await update(self, session=session)
@classmethod
async def get_by_id(
self, id: int, session: AsyncSession, select_in_load: List = []
) -> Optional[SQLModel]:
res = await read_where(
self, self.id == id, select_in_load=select_in_load, session=session
)
return res
@classmethod
async def get_where(self, *args, session: AsyncSession):
res = await read_where(self, *args, session=session)
return res
@classmethod
async def get_all_where(self, *args, session: AsyncSession):
res = await read_all_where(self, *args, session=session)
return res
async def delete(self, session: AsyncSession) -> None:
await delete(self, session=session)
async def update_from(self, data, session: AsyncSession) -> None:
data_dict = data.dict(exclude_unset=True)
for key in data_dict.keys():
setattr(self, key, getattr(data, key))
await update(self, session=session)
@classmethod
async def delete_by_id(self, id: int, session: AsyncSession) -> None:
res = await read_where(self, self.id == id, session=session)
await delete(res, session=session)

View File

@@ -0,0 +1,26 @@
from typing import Optional
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import Field, Relationship
from src.database.crud import read_where
from src.models.SQLModelExtended import SQLModelExtended
class Login(BaseModel):
email: str
password: str
class Config:
orm_mode = True
class User(SQLModelExtended, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
email: str
hashed_password: str
@classmethod
async def get_by_email(self, email: str, session: AsyncSession) -> "User":
return await read_where(self, self.email == email, session=session)

View File

@@ -0,0 +1,61 @@
from typing import List
import requests
from pydantic import BaseModel
from sqlmodel import Field, Relationship, SQLModel
from src.exceptions.base_exception import BaseException
from src.models.signvideo import SignVideo
from src.models.SQLModelExtended import SQLModelExtended
class Sign(SQLModelExtended, table=True):
id: int = Field(primary_key=True)
url: str
name: str
sign_id: str
video_url: str
sign_videos: List[SignVideo] = Relationship(
back_populates="sign",
sa_relationship_kwargs={"lazy": "selectin"},
)
def __init__(self, url):
self.url = url
# get name and sign id from url
try:
t = self.url.split("/")[-1].split("?")
self.name = t[0]
self.sign_id = t[1].split("=")[1]
except:
raise BaseException(404, "Invalid url")
self.get_video_url()
def get_video_url(self):
try:
r = requests.get(f"https://woordenboek.vlaamsegebarentaal.be/api/glosses/{self.name}")
res_json = r.json()
# find the video url
for item in res_json["variants"]:
if str(item["signId"]) == str(self.sign_id):
self.video_url = item["video"]
break
except:
raise BaseException(404, "Invalid url")
# throw exception if video url not found
if self.video_url is None:
raise BaseException(404, "Video url not found")
class SignOut(BaseModel):
id: int
url: str
name: str
sign_id: str
video_url: str
sign_videos: List[SignVideo] = []

View File

@@ -0,0 +1,25 @@
from pydantic import BaseModel
from sqlmodel import Field, Relationship, SQLModel
from src.exceptions.base_exception import BaseException
from src.models.SQLModelExtended import SQLModelExtended
class SignVideo(SQLModelExtended, table=True):
id: int = Field(primary_key=True)
approved: bool = False
# foreign key to sign
sign_id: int = Field(default=None, foreign_key="sign.id")
sign: "Sign" = Relationship(
back_populates="sign_videos",
sa_relationship_kwargs={"lazy": "selectin"},
)
# path where video saved
path: str
class SignVideoOut(BaseModel):
id: int
approved: bool

View File

@@ -0,0 +1,10 @@
from pydantic import BaseModel
class TokenExtended(BaseModel):
"""an output model for the tokens (accessToken + refreshToken)"""
id: str
access_token: str
access_token_expiry: int
refresh_token: str

View File

@@ -0,0 +1,3 @@
from .auth import router as auth_router
from .signs import router as signs_router
from .signvideo import router as signvideo_router

View File

@@ -0,0 +1,60 @@
import datetime
from fastapi import APIRouter, Depends
from fastapi_jwt_auth import AuthJWT
from sqlalchemy.ext.asyncio import AsyncSession
import src.settings as settings
from src.database.database import get_session
from src.exceptions.login_exception import LoginException
from src.models.auth import Login, User
from src.models.token import TokenExtended
from src.utils.cryptography import verify_password
router = APIRouter(prefix="/auth")
@router.post("/login")
async def login(creds: Login, Authorize: AuthJWT = Depends(), session: AsyncSession = Depends(get_session)):
# check if user exists
user = await User.get_by_email(creds.email, session=session)
# check if password is correct
if user is None or not verify_password(creds.password, user.hashed_password):
raise LoginException("Invalid login credentials")
access_token = Authorize.create_access_token(
subject=user.id,
expires_time=datetime.timedelta(seconds=settings.ACCESS_EXPIRES),
)
refresh_token = Authorize.create_refresh_token(
subject=user.id,
expires_time=datetime.timedelta(seconds=settings.REFRESH_EXPIRES),
)
return TokenExtended(
id=str(user.id),
access_token=access_token,
refresh_token=refresh_token,
access_token_expiry=settings.ACCESS_EXPIRES,
)
@router.get("/refresh")
async def refresh(Authorize: AuthJWT = Depends()):
Authorize.jwt_refresh_token_required()
current_user = Authorize.get_jwt_subject()
access_token = Authorize.create_access_token(
subject=current_user.id,
expires_time=datetime.timedelta(seconds=settings.ACCESS_EXPIRES),
)
refresh_token = Authorize.create_refresh_token(
subject=current_user.id,
expires_time=datetime.timedelta(seconds=settings.REFRESH_EXPIRES),
)
return TokenExtended(
id=str(current_user.id),
access_token=access_token,
refresh_token=refresh_token,
access_token_expiry=settings.ACCESS_EXPIRES,
)

View File

@@ -0,0 +1,123 @@
import datetime
import os
import zipfile
from fastapi import APIRouter, Depends, status
from fastapi.responses import FileResponse
from fastapi_jwt_auth import AuthJWT
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
import src.settings as settings
from src.database.database import get_session
from src.exceptions.login_exception import LoginException
from src.models.auth import Login, User
from src.models.sign import Sign, SignOut
from src.models.signvideo import SignVideo
from src.models.token import TokenExtended
router = APIRouter(prefix="/signs")
class SignUrl(BaseModel):
url: str
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=SignOut)
async def add_sign(url: SignUrl, Authorize: AuthJWT = Depends(), session: AsyncSession = Depends(get_session)):
Authorize.jwt_required()
user = Authorize.get_jwt_subject()
user = await User.get_by_id(id=user, session=session)
if not user:
raise LoginException("User not found")
sign = Sign(url.url)
# check if the sign already exists
signs = await Sign.get_all_where(Sign.url == sign.url, session=session)
if len(signs) > 0:
raise LoginException("Sign already exists")
await sign.save(session=session)
return sign
@router.get("/", status_code=status.HTTP_200_OK, response_model=list[SignOut])
async def get_signs(Authorize: AuthJWT = Depends(), session: AsyncSession = Depends(get_session)):
Authorize.jwt_required()
user = Authorize.get_jwt_subject()
user = await User.get_by_id(id=user, session=session)
if not user:
raise LoginException("User not found")
signs = await Sign.get_all(select_in_load=[Sign.sign_videos],session=session)
return signs
@router.get("/{id}", status_code=status.HTTP_200_OK, response_model=SignOut)
async def get_sign(id: int, Authorize: AuthJWT = Depends(), session: AsyncSession = Depends(get_session)):
Authorize.jwt_required()
user = Authorize.get_jwt_subject()
user = await User.get_by_id(id=user, session=session)
if not user:
raise LoginException("User not found")
sign = await Sign.get_by_id(id=id, select_in_load=[Sign.sign_videos], session=session)
return sign
@router.delete("/{id}", status_code=status.HTTP_200_OK)
async def delete_sign(id: int, Authorize: AuthJWT = Depends(), session: AsyncSession = Depends(get_session)):
Authorize.jwt_required()
user = Authorize.get_jwt_subject()
user = await User.get_by_id(id=user, session=session)
if not user:
raise LoginException("User not found")
sign = await Sign.get_by_id(id=id, select_in_load=[Sign.sign_videos], session=session)
if not sign:
raise LoginException("Sign not found")
# delete the sign videos
for sign_video in sign.sign_videos:
# get path of the sign video
path = f"{settings.DATA_PATH}/{sign_video.path}"
# delete the file
os.remove(path)
await sign_video.delete(session=session)
await sign.delete(session=session)
return {"message": "Sign deleted"}
@router.get("/download/all", status_code=status.HTTP_200_OK)
async def download_all(Authorize: AuthJWT = Depends(), session: AsyncSession = Depends(get_session)):
Authorize.jwt_required()
user = Authorize.get_jwt_subject()
user = await User.get_by_id(id=user, session=session)
if not user:
raise LoginException("User not found")
# get the approved sign videos
signs = await SignVideo.get_all_where(SignVideo.approved == True, session=session)
# extract the paths from the sign videos
paths = [sign.path for sign in signs]
zip_path = f"/tmp/{datetime.datetime.now().timestamp()}.zip"
# create the zip file
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, False) as zip_file:
for path in paths:
zip_file.write(f"{settings.DATA_PATH}/{path}", os.path.basename(path))
return FileResponse(zip_path, media_type="application/zip", filename="signs.zip")

View File

@@ -0,0 +1,170 @@
import base64
import datetime
import io
import os
import random
import string
import subprocess
from fastapi import APIRouter, Depends, FastAPI, File, UploadFile, status
from fastapi.responses import FileResponse
from fastapi_jwt_auth import AuthJWT
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import StreamingResponse
import src.settings as settings
from src.database.database import get_session
from src.exceptions.login_exception import LoginException
from src.models.auth import User
from src.models.sign import Sign
from src.models.signvideo import SignVideo, SignVideoOut
from src.utils.cryptography import verify_password
def extract_thumbnail(video_path):
proc = subprocess.run(["ffmpeg", "-i", video_path, "-ss", "00:00:02.000", "-vframes", "1", "-f", "image2pipe", "-"], stdout=subprocess.PIPE)
byte_data = proc.stdout
return byte_data
router = APIRouter(prefix="/signs/{sign_id}/video")
# endpoint to upload a file and save it in the data folder
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=SignVideoOut)
async def sign_video(
sign_id: int,
video: UploadFile,
session: AsyncSession = Depends(get_session),
Authorize: AuthJWT = Depends(),
):
Authorize.jwt_required()
current_user_id: int = Authorize.get_jwt_subject()
user = await User.get_by_id(current_user_id, session)
if not user:
raise LoginException("User not found")
sign = await Sign.get_by_id(sign_id, session)
if not sign:
raise LoginException("Sign not found")
video.filename = f"{sign.name}!{sign_id}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')}_{''.join(random.choices(string.ascii_uppercase + string.digits, k=5))}.mp4"
# create the data folder if not exists
if not os.path.exists(settings.DATA_PATH):
os.makedirs(settings.DATA_PATH)
sign_video = SignVideo(
sign_id=sign_id,
path=video.filename
)
with open(settings.DATA_PATH + "/" + video.filename + ".test", 'wb') as f:
f.write(video.file.read())
command = ['ffmpeg', '-y', '-i', settings.DATA_PATH + "/" + video.filename + ".test", '-c:v', 'libx264', '-c:a', 'aac', '-strict', '-2', '-b:a', '192k', '-c:a', 'aac', '-strict', '-2', '-b:a', '192k', settings.DATA_PATH + "/" + video.filename]
subprocess.run(command, check=True)
# delete the temporary file
os.remove(settings.DATA_PATH + "/" + video.filename + ".test")
await sign_video.save(session)
return sign_video
@router.get("/{video_id}/thumbnail", status_code=status.HTTP_200_OK)
async def sign_video_thumbnail(
sign_id: int,
video_id: int,
session: AsyncSession = Depends(get_session),
Authorize: AuthJWT = Depends(),
):
Authorize.jwt_required()
current_user_id: int = Authorize.get_jwt_subject()
user = await User.get_by_id(current_user_id, session)
if not user:
raise LoginException("User not found")
sign = await Sign.get_by_id(sign_id, session)
if not sign:
raise LoginException("Sign not found")
sign_video = await SignVideo.get_by_id(video_id, session)
if not sign_video:
raise BaseException("Sign video not found")
# extract the thumbnail from the video
bytes = extract_thumbnail(f"{settings.DATA_PATH}/{sign_video.path}")
return StreamingResponse(io.BytesIO(bytes), media_type="image/png")
@router.get("/{video_id}", status_code=status.HTTP_200_OK)
async def sign_video(
sign_id: int,
video_id: int,
session: AsyncSession = Depends(get_session),
Authorize: AuthJWT = Depends(),
):
Authorize.jwt_required()
current_user_id: int = Authorize.get_jwt_subject()
user = await User.get_by_id(current_user_id, session)
if not user:
raise LoginException("User not found")
sign = await Sign.get_by_id(sign_id, session)
if not sign:
raise LoginException("Sign not found")
sign_video = await SignVideo.get_by_id(video_id, session)
if not sign_video:
raise BaseException("Sign video not found")
# return the video
return FileResponse(f"{settings.DATA_PATH}/{sign_video.path}", media_type="video/mp4")
class SignVideoUpdate(BaseModel):
approved: bool
@router.patch("/{video_id}", status_code=status.HTTP_200_OK)
async def sign_video(
sign_id: int,
video_id: int,
update: SignVideoUpdate,
session: AsyncSession = Depends(get_session),
Authorize: AuthJWT = Depends(),
):
Authorize.jwt_required()
current_user_id: int = Authorize.get_jwt_subject()
user = await User.get_by_id(current_user_id, session)
if not user:
raise LoginException("User not found")
sign = await Sign.get_by_id(sign_id, session)
if not sign:
raise LoginException("Sign not found")
sign_video = await SignVideo.get_by_id(video_id, session)
if not sign_video:
raise BaseException("Sign video not found")
sign_video.approved = update.approved
await sign_video.save(session)
@router.delete("/{video_id}", status_code=status.HTTP_200_OK)
async def sign_video(
sign_id: int,
video_id: int,
session: AsyncSession = Depends(get_session),
Authorize: AuthJWT = Depends(),
):
Authorize.jwt_required()
current_user_id: int = Authorize.get_jwt_subject()
user = await User.get_by_id(current_user_id, session)
if not user:
raise LoginException("User not found")
sign = await Sign.get_by_id(sign_id, session)
if not sign:
raise LoginException("Sign not found")
sign_video = await SignVideo.get_by_id(video_id, session)
if not sign_video:
raise BaseException("Sign video not found")
await sign_video.delete(session)
# delete the video
os.remove(f"{settings.DATA_PATH}/{sign_video.path}")

33
backend/src/settings.py Normal file
View File

@@ -0,0 +1,33 @@
import os
from dotenv import load_dotenv
load_dotenv()
"""Testing"""
TEST_MODE: bool = os.getenv("TEST_MODE", False)
"""Database"""
DB_NAME: str = os.getenv("DB_NAME", "db")
DB_USER: str = os.getenv("DB_USER", "postgres")
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "postgres")
DB_HOST: str = os.getenv("DB_HOST", "localhost")
DB_PORT: str = os.getenv("DB_PORT", "5432")
DB_USE_SQLITE: bool = os.getenv("DB_USE_SQLITE", False)
DB_SQLITE_PATH: str = os.getenv("DB_SQLITE_PATH", "./sqlite.db")
"""Storage"""
DATA_PATH: str = os.getenv("DATA_PATH", "data")
"""Authentication"""
JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY")
ACCESS_EXPIRES: int = int(os.getenv("ACCESS_EXPIRES", 360000))
REFRESH_EXPIRES: int = int(os.getenv("REFRESH_EXPIRES", 864000))
"""Standard User"""
DEFAULT_USER_ENABLED: bool = os.getenv("DEFAULT_USER_ENABLED", False)
DEFAULT_USER_EMAIL: str = os.getenv("DEFAULT_USER_EMAIL", "test@test.com")
DEFAULT_USER_PASSWORD: str = os.getenv("DEFAULT_USER_PASSWORD", "test")

View File

@@ -0,0 +1,30 @@
""" This module includes the functions used to hash and verify passwords
"""
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""verify_password
:param plain_password: password entered by user
:type plain_password: str
:param hashed_password: hashed password saved in database
:type hashed_password: str
:return: hashed_password and plain_password matched
:rtype: bool
"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""get_password_hash get the hash of a password
:param password: the plain text password
:type password: str
:return: the hashed password from the plain text password
:rtype: str
"""
return pwd_context.hash(password)