Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
sverben committed Jan 21, 2024
0 parents commit 32a605c
Show file tree
Hide file tree
Showing 46 changed files with 4,971 additions and 0 deletions.
29 changes: 29 additions & 0 deletions .github/workflows/api.yaml
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
api/data
**/__pycache__
*/.venv
*/venv
api/database.db
*/.idea
*/.env
12 changes: 12 additions & 0 deletions api/Dockerfile
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"]
13 changes: 13 additions & 0 deletions api/app/conf.py
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 added api/app/db/__init__.py
Empty file.
225 changes: 225 additions & 0 deletions api/app/db/crud.py
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
22 changes: 22 additions & 0 deletions api/app/db/database.py
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()
47 changes: 47 additions & 0 deletions api/app/db/models.py
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)
Loading

0 comments on commit 32a605c

Please sign in to comment.