Skip to content

Commit

Permalink
Add image db field
Browse files Browse the repository at this point in the history
- Added DB field for storing plugin thumbnail path on CDN.
- Adjusted image upload to append file hash to the filename.
- Started using plugin thumbnail from DB field when it's set.
  • Loading branch information
gbdlin committed Sep 2, 2023
1 parent 21e5e0a commit 05418e3
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 40 deletions.
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
15 changes: 11 additions & 4 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,29 @@ 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)):
old_plugin = await db.get_plugin_by_id(db.session, data.id)
version_dates = {
version.name: version.created for version in (await db.get_plugin_by_id(db.session, data.id)).versions
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
@@ -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 = 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"
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

0 comments on commit 05418e3

Please sign in to comment.