feat: implement cdn mirrors endpoints, closes #15 (#17)

* feat: add cdn mirror endpoints

* refactor: change API version in docs

* docs: fix titles on API docs page
This commit is contained in:
Alexandre Teles 2022-10-13 01:06:50 -03:00 committed by GitHub
parent 24d78709fc
commit 4cb02c55ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 345 additions and 24 deletions

111
app/controllers/Mirrors.py Normal file
View File

@ -0,0 +1,111 @@
import toml
from redis import asyncio as aioredis
import app.utils.Logger as Logger
from app.models.MirrorModels import MirrorModel, MirrorStoreModel
from app.utils.RedisConnector import RedisConnector
config: dict = toml.load("config.toml")
class Mirrors:
"""Implements the Mirror class for the ReVanced API"""
redis = RedisConnector.connect(config['mirrors']['database'])
MirrorsLogger = Logger.MirrorsLogger()
async def assemble_key(self, org: str, repo: str, version: str) -> str:
"""Assemble the Redis key for the cdn
Returns:
str: The Redis key
"""
return f"{org}/{repo}/{version}"
async def store(self, org: str, repo: str, version: str, mirror: MirrorStoreModel) -> bool:
"""Store mirrors in the database
Args:
mirror (MirrorStoreModel): Pydantic model of the mirror information
Returns:
bool: True if data was stored successfully, False otherwise
"""
key = await self.assemble_key(org, repo, version)
mirror_payload: dict[str, str | list[str]] = {}
mirror_payload['cid'] = mirror.cid
mirror_payload['filenames'] = mirror.filenames
try:
await self.redis.json().set(key, '$', mirror_payload)
await self.MirrorsLogger.log("SET", None, key)
except aioredis.RedisError as e:
await self.MirrorsLogger.log("SET", e)
raise e
return True
async def exists(self, org: str, repo: str, version: str) -> bool:
"""Check if a cdn exists in the database
Returns:
bool: True if the cdn exists, False otherwise
"""
key = await self.assemble_key(org, repo, version)
try:
if await self.redis.exists(key):
await self.MirrorsLogger.log("EXISTS", None, key)
return True
else:
return False
except aioredis.RedisError as e:
await self.MirrorsLogger.log("EXISTS", e)
raise e
async def get(self, org: str, repo: str, version: str) -> MirrorModel:
"""Get the mirror information from the database
Returns:
dict[str, str | int]: The mirror information
"""
key = await self.assemble_key(org, repo, version)
try:
payload: dict[str, str | list[str]] = await self.redis.json().get(key)
mirror = MirrorModel(
repository=f"{org}/{repo}",
version=version,
cid=payload['cid'],
filenames=payload['filenames']
)
await self.MirrorsLogger.log("GET", None, key)
return mirror
except aioredis.RedisError as e:
await self.MirrorsLogger.log("GET", e)
raise e
async def delete(self, org: str, repo: str, version: str) -> bool:
"""Delete the cdn from the database
Returns:
bool: True if the cdn was deleted successfully, False otherwise
"""
key = await self.assemble_key(org, repo, version)
try:
await self.redis.delete(key)
await self.MirrorsLogger.log("DELETE", None, key)
return True
except aioredis.RedisError as e:
await self.MirrorsLogger.log("DELETE", e)
raise e

View File

@ -31,6 +31,7 @@ from app.routers import auth
from app.routers import tools
from app.routers import clients
from app.routers import patches
from app.routers import mirrors
from app.routers import changelogs
from app.routers import contributors
from app.routers import announcement
@ -74,6 +75,7 @@ app.include_router(changelogs.router)
app.include_router(auth.router)
app.include_router(clients.router)
app.include_router(announcement.router)
app.include_router(mirrors.router)
app.include_router(ping.router)
# Setup cache

View File

@ -48,4 +48,24 @@ class Unauthorized(BaseModel):
"""
error: str = "Unauthorized"
message: str = "The client is unauthorized to access this resource"
message: str = "The client is unauthorized to access this resource"
class MirrorNotFoundError(BaseModel):
"""Implements the response fields for when a mirror is not found.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
error: str = "Not Found"
message: str = "No mirror was found for the organization, repository, and version provided."
class MirrorAlreadyExistsError(BaseModel):
"""Implements the response fields for when a mirror already exists.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
error: str = "Conflict"
message: str = "A mirror already exists for the organization, repository, and version provided. Please use the PUT method to update the mirror."

View File

@ -0,0 +1,52 @@
from pydantic import BaseModel
class MirrorModel(BaseModel):
"""Implements the response fields for the CDN mirror.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
repository: str
version: str
cid: str
filenames: list[str]
class MirrorStoreModel(BaseModel):
"""Implements the fields for storing CDN mirror information.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
cid: str
filenames: list[str]
class MirrorCreatedResponseModel(BaseModel):
"""Implements the response fields for stored CDN mirrors.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
created: bool
key: str
class MirrorUpdatedResponseModel(BaseModel):
"""Implements the response fields for updated CDN mirrors.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
updated: bool
key: str
class MirrorDeletedResponseModel(BaseModel):
"""Implements the response fields for deleted CDN mirrors.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
deleted: bool
key: str

139
app/routers/mirrors.py Normal file
View File

@ -0,0 +1,139 @@
from fastapi_paseto_auth import AuthPASETO
from fastapi import APIRouter, Request, Response, Depends, status, HTTPException
from app.dependencies import load_config
from fastapi_cache.decorator import cache
from app.controllers.Clients import Clients
from app.controllers.Mirrors import Mirrors
import app.models.MirrorModels as MirrorModels
import app.models.GeneralErrors as GeneralErrors
router = APIRouter(
prefix="/mirrors",
tags=['CDN Mirrors']
)
clients = Clients()
mirrors = Mirrors()
config: dict = load_config()
@router.get('/{org}/{repo}/{version}', status_code=status.HTTP_200_OK, response_model=MirrorModels.MirrorModel)
async def get_mirrors(request: Request, response: Response, org: str, repo: str, version: str) -> MirrorModels.MirrorModel:
"""Get CDN mirror information for a given release.
Returns:
json: mirror information
"""
if not await mirrors.exists(org, repo, version):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail={
"error": GeneralErrors.MirrorNotFoundError().error,
"message": GeneralErrors.MirrorNotFoundError().message
}
)
else:
return await mirrors.get(org, repo, version)
@router.post('/{org}/{repo}/{version}', status_code=status.HTTP_201_CREATED, response_model=MirrorModels.MirrorCreatedResponseModel)
async def create_mirror(request: Request, response: Response, org: str, repo: str, version: str,
mirror: MirrorModels.MirrorStoreModel, Authorize: AuthPASETO = Depends()) -> dict:
"""Stores information about a new CDN mirror for a given release.
Returns:
bool: True if successful, False otherwise
"""
Authorize.paseto_required()
if await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()):
if await mirrors.exists(org, repo, version):
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail={
"error": GeneralErrors.MirrorAlreadyExistsError().error,
"message": GeneralErrors.MirrorAlreadyExistsError().message
}
)
else:
key = await mirrors.assemble_key(org, repo, version)
created: bool = await mirrors.store(org, repo, version, mirror)
if created:
return {"created": created, "key": key}
else:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail={
"error": GeneralErrors.InternalServerError().error,
"message": GeneralErrors.InternalServerError().message
}
)
else:
raise HTTPException(status_code=401, detail={
"error": GeneralErrors.Unauthorized().error,
"message": GeneralErrors.Unauthorized().message
}
)
@router.put('/{org}/{repo}/{version}', status_code=status.HTTP_200_OK, response_model=MirrorModels.MirrorUpdatedResponseModel)
async def update_mirror(request: Request, response: Response, org: str, repo: str, version: str,
mirror: MirrorModels.MirrorStoreModel,Authorize: AuthPASETO = Depends()) -> dict:
"""Updates a stored information about CDN mirrors for a given release
Returns:
bool: True if successful, False otherwise
"""
Authorize.paseto_required()
if await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()):
if not await mirrors.exists(org, repo, version):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail={
"error": GeneralErrors.MirrorNotFoundError().error,
"message": GeneralErrors.MirrorNotFoundError().message
}
)
else:
key = await mirrors.assemble_key(org, repo, version)
updated: bool = await mirrors.store(org, repo, version, mirror)
if updated:
return {"updated": updated, "key": key}
else:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail={
"error": GeneralErrors.InternalServerError().error,
"message": GeneralErrors.InternalServerError().message
}
)
else:
raise HTTPException(status_code=401, detail={
"error": GeneralErrors.Unauthorized().error,
"message": GeneralErrors.Unauthorized().message
}
)
@router.delete('/{org}/{repo}/{version}', status_code=status.HTTP_200_OK, response_model=MirrorModels.MirrorDeletedResponseModel)
async def delete_mirror(request: Request, response: Response, org: str, repo: str, version: str, Authorize: AuthPASETO = Depends()) -> dict:
"""Deletes a stored information about CDN mirrors for a given release
Returns:
json: _description_
"""
Authorize.paseto_required()
if await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()):
if not await mirrors.exists(org, repo, version):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail={
"error": GeneralErrors.MirrorNotFoundError().error,
"message": GeneralErrors.MirrorNotFoundError().message
}
)
else:
key = await mirrors.assemble_key(org, repo, version)
deleted: bool = await mirrors.delete(org, repo, version)
if deleted:
return {"deleted": deleted, "key": key}
else:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail={
"error": GeneralErrors.InternalServerError().error,
"message": GeneralErrors.InternalServerError().message
}
)
else:
raise HTTPException(status_code=401, detail={
"error": GeneralErrors.Unauthorized().error,
"message": GeneralErrors.Unauthorized().message
}
)

View File

@ -1,5 +1,3 @@
import sys
import logging
from loguru import logger
from redis import RedisError
from argon2.exceptions import VerifyMismatchError
@ -66,19 +64,15 @@ class AnnouncementsLogger:
else:
logger.info(f"[User] REDIS {operation} {key} - OK")
def setup_logging(LOG_LEVEL: str, JSON_LOGS: bool) -> None:
"""Setup logging for uvicorn and FastAPI."""
# intercept everything at the root logger
logging.root.handlers = [InterceptHandler()]
logging.root.setLevel(LOG_LEVEL)
# remove every other logger's handlers
# and propagate to root logger
for name in logging.root.manager.loggerDict.keys():
logging.getLogger(name).handlers = []
logging.getLogger(name).propagate = True
# configure loguru
logger.configure(handlers=[{"sink": sys.stdout, "serialize": JSON_LOGS}])
class MirrorsLogger:
async def log(self, operation: str, result: RedisError | None = None, key: str = "") -> None:
"""Logs internal cache operations
Args:
operation (str): Operation name
key (str): Key used in the operation
"""
if type(result) is RedisError:
logger.error(f"[User] REDIS {operation} - Failed with error: {result}")
else:
logger.info(f"[User] REDIS {operation} {key} - OK")

View File

@ -2,28 +2,28 @@
title = "ReVanced Releases API"
description = """
# The official JSON API for ReVanced Releases 🚀
## The official JSON API for ReVanced Releases 🚀
## Links
### Links
- [Changelogs](https://github.com/revanced/)
- [Official links to ReVanced](https://revanced.app)
## Important Information
### Important Information
* Rate Limiting - 60 requests per minute
* Cache - 5 minutes
* Token duration - 1 hour
* Token refresh - 30 days
## Additional Notes
### Additional Notes
1. Breaking changes are to be expected
2. Client side caching is adviced to avoid unnecessary requests
3. Abuse of the API will result in IP blocks
"""
version = "0.8.5 RC"
version = "0.9 RC2"
[license]
@ -51,6 +51,9 @@ database = 3
[announcements]
database = 4
[mirrors]
database = 5
[app]
repositories = ["TeamVanced/VancedMicroG", "revanced/revanced-cli", "revanced/revanced-patcher", "revanced/revanced-patches", "revanced/revanced-integrations", "revanced/revanced-manager"]