-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 32a605c
Showing
46 changed files
with
4,971 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
name: Docker | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
workflow_dispatch: {} | ||
|
||
jobs: | ||
push: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v3 | ||
|
||
- name: Docker Login | ||
uses: docker/[email protected] | ||
with: | ||
registry: ghcr.io | ||
username: ${{ github.actor }} | ||
password: ${{ secrets.GITHUB_TOKEN }} | ||
|
||
- name: Build and push Docker image | ||
uses: docker/[email protected] | ||
with: | ||
file: Dockerfile | ||
context: api | ||
push: true | ||
tags: ghcr.io/djoamersfoort/notifications/api:latest |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
api/data | ||
**/__pycache__ | ||
*/.venv | ||
*/venv | ||
api/database.db | ||
*/.idea | ||
*/.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
FROM python:3.11-slim | ||
|
||
COPY app/requirements.txt /tmp/requirements.txt | ||
RUN pip install --no-cache-dir -r /tmp/requirements.txt | ||
|
||
RUN useradd -m -U -d /app media | ||
USER media | ||
WORKDIR /app | ||
|
||
COPY app /app | ||
|
||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
from pydantic_settings import BaseSettings, SettingsConfigDict | ||
|
||
|
||
class Settings(BaseSettings): | ||
model_config = SettingsConfigDict(env_file=".env") | ||
|
||
base_url: str | ||
allowed_users: list[str] | ||
openid_configuration: str | ||
database_url: str | ||
|
||
|
||
settings = Settings() |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
import datetime | ||
import os | ||
from functools import lru_cache | ||
from uuid import uuid4, UUID | ||
|
||
import ffmpeg | ||
from Crypto.Hash import SHA256 | ||
from Crypto.PublicKey import RSA | ||
from Crypto.Signature import PKCS1_v1_5 | ||
from fastapi import UploadFile | ||
from fastapi.responses import FileResponse | ||
from sqlalchemy.orm import Session | ||
|
||
from app.conf import settings | ||
from app.db import models, schemas | ||
|
||
|
||
@lru_cache() | ||
def get_signer(): | ||
with open("data/private.key", "r") as buffer: | ||
key = buffer.read() | ||
|
||
return PKCS1_v1_5.new(RSA.import_key(key)) | ||
|
||
|
||
@lru_cache() | ||
def get_verifier(): | ||
with open("data/public.key", "r") as buffer: | ||
key = buffer.read() | ||
|
||
return PKCS1_v1_5.new(RSA.import_key(key)) | ||
|
||
|
||
def sign_url(url: str): | ||
signature = get_signer().sign(SHA256.new(url.encode("utf-8"))) | ||
return f"{url}?signature={signature.hex()}" | ||
|
||
|
||
def sign_item(item_data: models.Item): | ||
item = schemas.Item.model_validate(item_data) | ||
|
||
item.cover_path = sign_url( | ||
f"{settings.base_url}/items/{item_data.album_id}/{item.id}/cover" | ||
) | ||
item.path = sign_url( | ||
f"{settings.base_url}/items/{item_data.album_id}/{item.id}/full" | ||
) | ||
|
||
return item | ||
|
||
|
||
def get_album(db: Session, album_id: UUID): | ||
album_data = db.query(models.Album).filter(models.Album.id == album_id).first() | ||
album = schemas.Album.model_validate(album_data) | ||
album.items = [] | ||
|
||
for item in album_data.items: | ||
album.items.append(sign_item(item)) | ||
|
||
return album | ||
|
||
|
||
def get_full(db: Session, item_id: UUID): | ||
item = db.query(models.Item).filter(models.Item.id == item_id).first() | ||
|
||
return FileResponse(item.path) | ||
|
||
|
||
def get_cover(db: Session, item_id: UUID): | ||
item = db.query(models.Item).filter(models.Item.id == item_id).first() | ||
|
||
return FileResponse(item.cover_path) | ||
|
||
|
||
def get_albums(db: Session): | ||
albums = db.query(models.Album).all() | ||
result = [] | ||
|
||
for album_data in albums: | ||
album = schemas.AlbumList.model_validate(album_data) | ||
if album_data.preview_id: | ||
album.preview = sign_item(album_data.preview) | ||
|
||
result.append(album) | ||
|
||
return result | ||
|
||
|
||
def create_album(db: Session, album: schemas.AlbumCreate): | ||
album_id = uuid4() | ||
os.mkdir(f"data/items/{album_id}") | ||
|
||
total_albums = db.query(models.Album).count() | ||
db_album = models.Album( | ||
id=album_id, name=album.name, description=album.description, order=total_albums | ||
) | ||
db.add(db_album) | ||
db.commit() | ||
db.refresh(db_album) | ||
|
||
return db_album | ||
|
||
|
||
def update_album(db: Session, album_id: UUID, album: schemas.AlbumCreate): | ||
db_album = db.query(models.Album).filter(models.Album.id == album_id).first() | ||
db_album.name = album.name | ||
db_album.description = album.description | ||
db.commit() | ||
|
||
return db_album | ||
|
||
|
||
def order_albums(db: Session, albums: list[schemas.AlbumOrder]): | ||
for album in albums: | ||
db_album = db.query(models.Album).filter(models.Album.id == album.id).first() | ||
db_album.order = album.order | ||
db.commit() | ||
return db.query(models.Album).all() | ||
|
||
|
||
async def create_item( | ||
db: Session, | ||
user: schemas.User, | ||
items: list[UploadFile], | ||
album_id: UUID, | ||
date: datetime = None, | ||
): | ||
db_items = [] | ||
|
||
for item in items: | ||
# write temp file | ||
item_id = uuid4() | ||
os.mkdir(f"data/items/{album_id}/{item_id}") | ||
with open(f"/tmp/{item_id}", "wb") as buffer: | ||
buffer.write(await item.read()) | ||
|
||
# get type | ||
file_type = item.content_type.split("/")[0] | ||
if file_type not in ["image", "video"]: | ||
continue | ||
|
||
if file_type == "image": | ||
file_type = models.Type.IMAGE | ||
else: | ||
file_type = models.Type.VIDEO | ||
|
||
# get metadata | ||
probe = ffmpeg.probe(f"/tmp/{item_id}") | ||
width = probe["streams"][0]["width"] | ||
height = probe["streams"][0]["height"] | ||
|
||
# create cover image | ||
cover_path = f"data/items/{album_id}/{item_id}/cover.jpg" | ||
|
||
if file_type == models.Type.VIDEO: | ||
stream = ffmpeg.input(f"/tmp/{item_id}") | ||
stream = ffmpeg.filter(stream, "scale", 400, -1) | ||
stream = ffmpeg.output(stream, cover_path, vframes=1) | ||
ffmpeg.run(stream) | ||
else: | ||
stream = ffmpeg.input(f"/tmp/{item_id}") | ||
stream = ffmpeg.filter(stream, "scale", 400, -1) | ||
stream = ffmpeg.output(stream, cover_path) | ||
ffmpeg.run(stream) | ||
|
||
# store optimized full size image/video | ||
if file_type == models.Type.VIDEO: | ||
path = f"data/items/{album_id}/{item_id}/item.mp4" | ||
|
||
stream = ffmpeg.input(f"/tmp/{item_id}") | ||
stream = ffmpeg.output(stream, path, crf=23) | ||
ffmpeg.run(stream) | ||
else: | ||
path = f"data/items/{album_id}/{item_id}/item.jpg" | ||
|
||
stream = ffmpeg.input(f"/tmp/{item_id}") | ||
stream = ffmpeg.output(stream, path) | ||
ffmpeg.run(stream) | ||
|
||
db_item = models.Item( | ||
id=item_id, | ||
user=user.id, | ||
album_id=album_id, | ||
type=file_type, | ||
width=width, | ||
height=height, | ||
cover_path=cover_path, | ||
path=path, | ||
date=date, | ||
) | ||
db.add(db_item) | ||
db.commit() | ||
db.refresh(db_item) | ||
db_items.append(db_item) | ||
|
||
os.remove(f"/tmp/{item_id}") | ||
|
||
return db_items | ||
|
||
|
||
def delete_items(db: Session, user: schemas.User, album_id: UUID, items: list[UUID]): | ||
for item in items: | ||
db_item = db.query(models.Item).filter(models.Item.id == item).first() | ||
if db_item.user != user.id and not user.admin: | ||
continue | ||
|
||
os.remove(db_item.path) | ||
os.remove(db_item.cover_path) | ||
os.rmdir(f"data/items/{album_id}/{item}") | ||
db.delete(db_item) | ||
db.commit() | ||
|
||
return db.query(models.Album).filter(models.Album.id == album_id).first() | ||
|
||
|
||
def set_preview(db: Session, album_id: UUID, item_id: UUID): | ||
db_album = db.query(models.Album).filter(models.Album.id == album_id).first() | ||
db_item = db.query(models.Item).filter(models.Item.id == item_id).first() | ||
if db_item.album_id != album_id: | ||
return db_album | ||
|
||
db_album.preview = db_item | ||
db.commit() | ||
|
||
return db_album |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
from sqlalchemy import create_engine | ||
from sqlalchemy.ext.declarative import declarative_base | ||
from sqlalchemy.orm import sessionmaker | ||
from app.conf import settings | ||
|
||
|
||
SQLALCHEMY_DATABASE_URL = settings.database_url | ||
|
||
engine = create_engine( | ||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} | ||
) | ||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) | ||
|
||
Base = declarative_base() | ||
|
||
|
||
def get_db(): | ||
db = SessionLocal() | ||
try: | ||
yield db | ||
finally: | ||
db.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import enum | ||
|
||
from sqlalchemy import Column, ForeignKey, String, Uuid, Enum, Integer, DateTime | ||
from sqlalchemy.orm import relationship | ||
from sqlalchemy.sql import func | ||
|
||
from app.db.database import Base | ||
|
||
|
||
class Type(enum.Enum): | ||
IMAGE = 1 | ||
VIDEO = 2 | ||
|
||
|
||
class Album(Base): | ||
__tablename__ = "albums" | ||
|
||
id = Column(Uuid, primary_key=True, index=True) | ||
name = Column(String) | ||
description = Column(String) | ||
order = Column(Integer) | ||
preview_id = Column(Uuid, ForeignKey("items.id"), nullable=True) | ||
preview = relationship("Item", foreign_keys=[preview_id]) | ||
|
||
items = relationship( | ||
"Item", | ||
back_populates="album", | ||
order_by="desc(Item.date)", | ||
foreign_keys="Item.album_id", | ||
) | ||
|
||
|
||
class Item(Base): | ||
__tablename__ = "items" | ||
|
||
id = Column(Uuid, primary_key=True, index=True) | ||
user = Column(String) | ||
album_id = Column(Uuid, ForeignKey("albums.id")) | ||
album = relationship("Album", back_populates="items", foreign_keys=[album_id]) | ||
date = Column(DateTime(timezone=True), server_default=func.now()) | ||
|
||
type = Column(Enum(Type)) | ||
width = Column(String) | ||
height = Column(String) | ||
|
||
cover_path = Column(String) | ||
path = Column(String) |
Oops, something went wrong.