Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add image db field #49

Merged
merged 2 commits into from
Sep 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Auto detect text files and perform LF normalization
* text=auto
*.png filter=lfs diff=lfs merge=lfs -text
2 changes: 1 addition & 1 deletion alembic.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ script_location = /app/plugin_store/database/migrations

# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s

# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
Expand Down
17 changes: 11 additions & 6 deletions plugin_store/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ async def submit_release(
await db.delete_plugin(db.session, plugin.id)
plugin = None

image_path = await upload_image(data.name, data.image)

if plugin is not None:
if data.version_name in [i.name for i in plugin.versions]:
raise HTTPException(status_code=400, detail="Version already exists")
Expand All @@ -106,6 +108,7 @@ async def submit_release(
plugin,
author=data.author,
description=data.description,
image_path=image_path,
tags=list(filter(None, reduce(add, (el.split(",") for el in data.tags), []))),
)
else:
Expand All @@ -114,25 +117,27 @@ async def submit_release(
name=data.name,
author=data.author,
description=data.description,
image_path=image_path,
tags=list(filter(None, reduce(add, (el.split(",") for el in data.tags), []))),
)

version = await db.insert_version(db.session, plugin.id, name=data.version_name, **await upload_version(data.file))

await db.session.refresh(plugin)

await upload_image(plugin, data.image)
await post_announcement(plugin, version)
return plugin


@app.post("/__update", dependencies=[Depends(auth_token)], response_model=api_update.UpdatePluginResponse)
async def update_plugin(data: "api_update.UpdatePluginRequest", db: "Database" = Depends(database)):
version_dates = {
version.name: version.created for version in (await db.get_plugin_by_id(db.session, data.id)).versions
}
old_plugin = await db.get_plugin_by_id(db.session, data.id)
version_dates = {version.name: version.created for version in old_plugin.versions}
await db.delete_plugin(db.session, data.id)
new_plugin = await db.insert_artifact(db.session, **data.dict(exclude={"versions"}))
new_plugin = await db.insert_artifact(
db.session,
image_path=old_plugin._image_path,
**data.dict(exclude={"versions"}),
)

for version in reversed(data.versions):
await db.insert_version(
Expand Down
37 changes: 30 additions & 7 deletions plugin_store/cdn.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,27 @@
from hashlib import sha1, sha256
from os import getenv
from typing import TYPE_CHECKING
from urllib.parse import quote

from aiohttp import ClientSession

from database.models import Artifact

if TYPE_CHECKING:
from fastapi import UploadFile


async def b2_upload(filename: str, binary: "bytes"):
IMAGE_TYPES = {
"image/png": ".png",
"image/jpeg": ".jpg",
"image/webp": ".webp",
"image/avif": ".avif",
}


def construct_image_path(plugin_name: str, file_hash: str, mime_type: str) -> str:
return f"artifact_images/{quote(plugin_name)}-{file_hash}{IMAGE_TYPES[mime_type]}"


async def b2_upload(filename: str, binary: "bytes", mime_type: str = "b2/x-auto"):
async with ClientSession(raise_for_status=True) as web:
auth_str = f"{getenv('B2_APP_KEY_ID')}:{getenv('B2_APP_KEY')}".encode("utf-8")
async with web.get(
Expand All @@ -37,7 +48,7 @@ async def b2_upload(filename: str, binary: "bytes"):
data=binary,
headers={
"Authorization": res_data["authorizationToken"],
"Content-Type": "b2/x-auto",
"Content-Type": mime_type,
"Content-Length": str(len(binary)),
"X-Bz-Content-Sha1": sha1(binary).hexdigest(),
"X-Bz-File-Name": filename,
Expand All @@ -46,11 +57,23 @@ async def b2_upload(filename: str, binary: "bytes"):
return await res_data.text()


async def upload_image(plugin: "Artifact", image_url: "str"):
async def fetch_image(image_url: str) -> "tuple[bytes, str] | None":
async with ClientSession() as web:
async with web.get(image_url) as res:
if res.status == 200 and res.headers.get("Content-Type") == "image/png":
await b2_upload(plugin.image_path, await res.read())
if res.status == 200 and (mime_type := res.headers.get("Content-Type")) in IMAGE_TYPES:
return await res.read(), mime_type
return None


async def upload_image(plugin_name: str, image_url: str) -> "str | None":
fetched = await fetch_image(image_url)
if fetched is not None:
binary, mime_type = fetched
file_hash = sha256(binary).hexdigest()
file_path = construct_image_path(plugin_name, file_hash, mime_type)
await b2_upload(file_path, binary)
return file_path
return None


async def upload_version(file: "UploadFile"):
Expand Down
12 changes: 11 additions & 1 deletion plugin_store/database/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,21 @@ async def insert_artifact(
author: "str",
description: "str",
tags: "list[str]",
image_path: "str | None" = None,
id: "int | None" = None,
visible: "bool" = True,
) -> "Artifact":
nested = await session.begin_nested()
async with self.lock:
tag_objs = await self.prepare_tags(session, tags)
plugin = Artifact(name=name, author=author, description=description, tags=tag_objs, visible=visible)
plugin = Artifact(
name=name,
author=author,
description=description,
_image_path=image_path,
tags=tag_objs,
visible=visible,
)
if id is not None:
plugin.id = id
try:
Expand All @@ -111,6 +119,8 @@ async def update_artifact(self, session: "AsyncSession", plugin: "Artifact", **k
plugin.author = kwargs["author"]
if "description" in kwargs:
plugin.description = kwargs["description"]
if "image_path" in kwargs:
plugin._image_path = kwargs["image_path"]
if "tags" in kwargs:
plugin.tags = await self.prepare_tags(session, kwargs["tags"])
try:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Revision ID: abe90daeb874
Revises: 4fc55239b4d6
Create Date: 2022-11-08 18:23:52.915293
Create Date: 2022-11-19 18:23:52.915293

"""
import sqlalchemy as sa
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,25 @@

def upgrade() -> None:
with op.batch_alter_table("artifacts") as batch_op_artifacts:
batch_op_artifacts.alter_column(
batch_op_artifacts.alter_column( # type: ignore[attr-defined]
"visible", existing_type=sa.BOOLEAN(), nullable=True, existing_server_default=sa.text("'1'")
)
with op.batch_alter_table("versions") as batch_op_versions:
batch_op_versions.add_column(sa.Column("file", sa.Text(), nullable=True))
batch_op_versions.create_unique_constraint("unique_version_artifact_id_name", ["artifact_id", "name"])
batch_op_versions.add_column(sa.Column("file", sa.Text(), nullable=True)) # type: ignore[attr-defined]
batch_op_versions.create_unique_constraint( # type: ignore[attr-defined]
"unique_version_artifact_id_name",
["artifact_id", "name"],
)


def downgrade() -> None:
with op.batch_alter_table("versions") as batch_op_versions:
batch_op_versions.drop_constraint("unique_version_artifact_id_name", type_="unique")
batch_op_versions.drop_column("versions", "file")
batch_op_versions.drop_constraint( # type: ignore[attr-defined]
"unique_version_artifact_id_name",
type_="unique",
)
batch_op_versions.drop_column("versions", "file") # type: ignore[attr-defined]
with op.batch_alter_table("artifacts") as batch_op_artifacts:
batch_op_artifacts.alter_column(
batch_op_artifacts.alter_column( # type: ignore[attr-defined]
"artifacts", "visible", existing_type=sa.BOOLEAN(), nullable=False, existing_server_default=sa.text("'1'")
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""add artifact image field

Revision ID: 00b050c80d6d
Revises: 492a599cd718
Create Date: 2023-06-26 00:57:41.153757

"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "00b050c80d6d"
down_revision = "492a599cd718"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.add_column("artifacts", sa.Column("image_path", sa.Text(), nullable=True))


def downgrade() -> None:
op.drop_column("artifacts", "image_path")
3 changes: 3 additions & 0 deletions plugin_store/database/models/Artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Artifact(Base):
name: str = Column(Text)
author: str = Column(Text)
description: str = Column(Text)
_image_path: "str | None" = Column("image_path", Text, nullable=True)
tags: "list[Tag]" = relationship(
"Tag", secondary=PluginTag, cascade="all, delete", order_by="Tag.tag", lazy="selectin"
)
Expand All @@ -56,4 +57,6 @@ def image_url(self):

@property
def image_path(self):
if self._image_path is not None:
return self._image_path
return f"artifact_images/{quote(self.name)}.png"
1 change: 1 addition & 0 deletions plugin_store/database/models/Version.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from sqlalchemy import Column, ForeignKey, Integer, Text, UniqueConstraint

import constants

from ..utils import TZDateTime
from .Base import Base

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ version = "0.1.0"
description = "Plugin Store backend for Decky"
authors = ["Your Name <[email protected]>"]
readme = "README.md"
packages = [{include = "decky_plugin_store"}]
packages = []

[tool.poetry.dependencies]
python = "^3.9"
Expand Down
23 changes: 18 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@
from freezegun.api import FrozenDateTimeFactory

APP_PATH = Path("./plugin_store").absolute()
TESTS_PATH = Path(__file__).expanduser().resolve().parent
DUMMY_DATA_PATH = TESTS_PATH / "dummy_data"


@pytest.fixture(scope="session", autouse=True)
def mock_external_services(session_mocker: "MockFixture"):
session_mocker.patch("cdn.b2_upload")
session_mocker.patch(
"cdn.fetch_image",
return_value=((DUMMY_DATA_PATH / "plugin-image.png").read_bytes(), "image/png"),
)
session_mocker.patch("discord.AsyncDiscordWebhook", new=session_mocker.AsyncMock)


Expand Down Expand Up @@ -98,6 +104,7 @@ async def create(
name: "str | None" = None,
author: "str | None" = None,
description: "str | None" = None,
image: "str | None" = None,
tags: "int | list[str] | None" = None,
versions: "int | list[str] | list[dict] | None" = None,
visible: bool = True,
Expand All @@ -122,6 +129,7 @@ async def create(
name=name,
author=author,
description=description,
image_path=image,
tags=tags,
visible=visible,
)
Expand Down Expand Up @@ -149,20 +157,25 @@ async def seed_db(db: "Database", db_sessionmaker: "sessionmaker", freezer: "Fro
freezer.move_to("2022-02-25T00:00:00Z")
await generator.create("plugin-1", tags=["tag-1", "tag-2"], versions=["0.1.0", "0.2.0", "1.0.0"])
freezer.move_to("2022-02-25T00:01:00Z")
await generator.create("plugin-2", tags=["tag-1", "tag-3"], versions=["1.1.0", "2.0.0"])
await generator.create("plugin-2", image="2.png", tags=["tag-2"], versions=["1.1.0", "2.0.0"])
freezer.move_to("2022-02-25T00:02:00Z")
await generator.create("third", tags=["tag-2", "tag-3"], versions=["3.0.0", "3.1.0", "3.2.0"])
freezer.move_to("2022-02-25T00:03:00Z")
await generator.create("plugin-4", tags=["tag-1"], versions=["1.0.0", "2.0.0", "3.0.0", "4.0.0"])
await generator.create("plugin-4", tags=["tag-1", "tag-3"], versions=["1.0.0", "2.0.0", "3.0.0", "4.0.0"])
freezer.move_to("2022-02-25T00:04:00Z")
await generator.create("plugin-5", tags=["tag-1", "tag-2"], versions=["0.1.0", "0.2.0", "1.0.0"], visible=False)
freezer.move_to("2022-02-25T00:05:00Z")
await generator.create("plugin-6", tags=["tag-1", "tag-3"], versions=["1.1.0", "2.0.0"], visible=False)
await generator.create("plugin-6", image="6.png", tags=["tag-2"], versions=["1.1.0", "2.0.0"], visible=False)
freezer.move_to("2022-02-25T00:06:00Z")
await generator.create("seventh", tags=["tag-2", "tag-3"], versions=["3.0.0", "3.1.0", "3.2.0"], visible=False)
freezer.move_to("2022-02-25T00:07:00Z")
await generator.create("plugin-8", tags=["tag-1"], versions=["1.0.0", "2.0.0", "3.0.0", "4.0.0"], visible=False)
session.commit()
await generator.create(
"plugin-8",
tags=["tag-1", "tag-3"],
versions=["1.0.0", "2.0.0", "3.0.0", "4.0.0"],
visible=False,
)
# session.commit()

return db

Expand Down
3 changes: 3 additions & 0 deletions tests/dummy_data/plugin-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading