Skip to content

Commit

Permalink
upload urdfs with stls (#296)
Browse files Browse the repository at this point in the history
* upload urdfs with stls

* format

* tests
  • Loading branch information
codekansas authored Aug 17, 2024
1 parent 2268484 commit 477e35f
Show file tree
Hide file tree
Showing 12 changed files with 130 additions and 71 deletions.
6 changes: 3 additions & 3 deletions frontend/src/components/listing/ListingDescription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ export const RenderDescription = ({ description }: RenderDescriptionProps) => {
<h4 className="text-md mb-2 font-bold">{children}</h4>
),
img: ({ src, alt }) => (
<div
<span
className="flex flex-col justify-center w-full mx-auto gap-2 my-4 md:w-2/3 lg:w-1/2 cursor-pointer"
onClick={() => src && setImageModal([src, alt ?? ""])}
>
<img src={src} alt={alt} className="rounded-lg" />
{alt && <p className="text-sm text-center">{alt}</p>}
</div>
{alt && <span className="text-sm text-center">{alt}</span>}
</span>
),
}}
>
Expand Down
14 changes: 4 additions & 10 deletions frontend/src/components/listing/MeshRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,9 @@ interface UrdfModelProps {

const UrdfModel = ({ url, meshType }: UrdfModelProps) => {
const ref = useRef<Group>();
const [robot, setRobot] = useState<Group>();
const geom = useLoader(URDFLoader, url);

const loader = new URDFLoader();

loader.load(url, (robot) => {
setRobot(robot);
});

return robot ? (
return (
<group>
<mesh
castShadow
Expand All @@ -66,7 +60,7 @@ const UrdfModel = ({ url, meshType }: UrdfModelProps) => {
>
<primitive
ref={ref}
object={robot}
object={geom}
position={[0, 0, 0]}
dispose={null}
castShadow
Expand All @@ -77,7 +71,7 @@ const UrdfModel = ({ url, meshType }: UrdfModelProps) => {
<shadowMaterial opacity={0.25} />
</Plane>
</group>
) : null;
);
};

interface StlModelProps {
Expand Down
3 changes: 0 additions & 3 deletions frontend/src/components/nav/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import {
FaDoorOpen,
FaHome,
FaKey,
FaLock,
FaPen,
FaQuestion,
FaScroll,
FaTimes,
FaUserCircle,
} from "react-icons/fa";
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/gen/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,15 +390,15 @@ export interface paths {
patch?: never;
trace?: never;
};
"/artifacts/url/{artifact_type}/{artifact_id}": {
"/artifacts/url/{artifact_type}/{listing_id}/{name}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Artifact Url */
get: operations["artifact_url_artifacts_url__artifact_type___artifact_id__get"];
get: operations["artifact_url_artifacts_url__artifact_type___listing_id___name__get"];
put?: never;
post?: never;
delete?: never;
Expand Down Expand Up @@ -630,6 +630,8 @@ export interface components {
ListArtifactsItem: {
/** Artifact Id */
artifact_id: string;
/** Listing Id */
listing_id: string;
/** Name */
name: string;
/**
Expand Down Expand Up @@ -1491,15 +1493,16 @@ export interface operations {
};
};
};
artifact_url_artifacts_url__artifact_type___artifact_id__get: {
artifact_url_artifacts_url__artifact_type___listing_id___name__get: {
parameters: {
query?: {
size?: "small" | "large";
};
header?: never;
path: {
artifact_type: "image" | "urdf" | "mjcf" | "stl";
artifact_id: string;
listing_id: string;
name: string;
};
cookie?: never;
};
Expand Down
36 changes: 17 additions & 19 deletions store/app/crud/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
class ArtifactsCrud(BaseCrud):
@classmethod
def get_gsis(cls) -> set[str]:
return super().get_gsis().union({"user_id", "listing_id"})
return super().get_gsis().union({"user_id", "listing_id", "name"})

async def _crop_image(self, image: Image.Image, size: tuple[int, int]) -> io.BytesIO:
# Simply squashes the image to the desired size.
Expand Down Expand Up @@ -59,10 +59,10 @@ async def _crop_image(self, image: Image.Image, size: tuple[int, int]) -> io.Byt
image_bytes.seek(0)
return image_bytes

async def _upload_cropped_image(self, image: Image.Image, name: str, image_id: str, size: ArtifactSize) -> None:
async def _upload_cropped_image(self, image: Image.Image, artifact: Artifact, size: ArtifactSize) -> None:
image_bytes = await self._crop_image(image, SizeMapping[size])
filename = get_artifact_name(image_id, "image", size)
await self._upload_to_s3(image_bytes, name, filename, "image/png")
filename = get_artifact_name(artifact=artifact, size=size)
await self._upload_to_s3(image_bytes, artifact.name, filename, "image/png")

async def _upload_image(
self,
Expand All @@ -86,15 +86,7 @@ async def _upload_image(
image = Image.open(file)

await asyncio.gather(
*(
self._upload_cropped_image(
image=image,
name=name,
image_id=artifact.id,
size=size,
)
for size in SizeMapping.keys()
),
*(self._upload_cropped_image(image=image, artifact=artifact, size=size) for size in SizeMapping.keys()),
self._add_item(artifact),
)
return artifact
Expand Down Expand Up @@ -129,7 +121,7 @@ async def _upload_stl(
description=description,
)
await asyncio.gather(
self._upload_to_s3(out_file, name, get_artifact_name(artifact.id, "stl"), content_type),
self._upload_to_s3(out_file, name, get_artifact_name(artifact=artifact), content_type),
self._add_item(artifact),
)
return artifact
Expand Down Expand Up @@ -169,7 +161,7 @@ async def _upload_xml(
description=description,
)
await asyncio.gather(
self._upload_to_s3(out_file, name, get_artifact_name(artifact.id, artifact_type), content_type),
self._upload_to_s3(out_file, name, get_artifact_name(artifact=artifact), content_type),
self._add_item(artifact),
)
return artifact
Expand All @@ -183,6 +175,10 @@ async def upload_artifact(
artifact_type: ArtifactType,
description: str | None = None,
) -> Artifact:
# Validates that the name is unique.
if await self.has_artifact_named(name):
raise BadArtifactError("An artifact with this name already exists")

match artifact_type:
case "image":
return await self._upload_image(name, file, listing, user_id, description)
Expand All @@ -197,20 +193,19 @@ async def _remove_image(self, artifact: Artifact, user_id: str) -> None:
if artifact.user_id != user_id:
raise NotAuthorizedError("User does not have permission to delete this image")
await asyncio.gather(
*(self._delete_from_s3(get_artifact_name(artifact.id, "image", size)) for size in SizeMapping.keys()),
*(self._delete_from_s3(get_artifact_name(artifact=artifact, size=size)) for size in SizeMapping.keys()),
self._delete_item(artifact),
)

async def _remove_raw_artifact(
self,
artifact: Artifact,
artifact_type: Literal["urdf", "mjcf", "stl"],
user_id: str,
) -> None:
if artifact.user_id != user_id:
raise NotAuthorizedError("User does not have permission to delete this artifact")
await asyncio.gather(
self._delete_from_s3(get_artifact_name(artifact.id, artifact_type)),
self._delete_from_s3(get_artifact_name(artifact=artifact)),
self._delete_item(artifact),
)

Expand All @@ -219,7 +214,7 @@ async def remove_artifact(self, artifact: Artifact, user_id: str) -> None:
case "image":
await self._remove_image(artifact, user_id)
case _:
await self._remove_raw_artifact(artifact, artifact.artifact_type, user_id)
await self._remove_raw_artifact(artifact, user_id)

async def get_listing_artifacts(self, listing_id: str) -> list[Artifact]:
artifacts = await self._get_items_from_secondary_index("listing_id", listing_id, Artifact)
Expand All @@ -229,6 +224,9 @@ async def get_listings_artifacts(self, listing_ids: list[str]) -> list[list[Arti
artifact_chunks = await self._get_items_from_secondary_index_batch("listing_id", listing_ids, Artifact)
return [sorted(artifacts, key=lambda a: a.timestamp) for artifacts in artifact_chunks]

async def has_artifact_named(self, filename: str) -> bool:
return len(await self._get_items_from_secondary_index("name", filename, Artifact)) > 0

async def edit_artifact(
self,
artifact_id: str,
Expand Down
9 changes: 9 additions & 0 deletions store/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from store.app.db import create_tables
from store.app.errors import (
BadArtifactError,
InternalError,
ItemNotFoundError,
NotAuthenticatedError,
Expand Down Expand Up @@ -91,6 +92,14 @@ async def not_authorized_exception_handler(request: Request, exc: NotAuthorizedE
)


@app.exception_handler(BadArtifactError)
async def bad_artifact_exception_handler(request: Request, exc: BadArtifactError) -> JSONResponse:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"message": "Bad artifact.", "detail": str(exc)},
)


@app.get("/")
async def read_root() -> bool:
return True
Expand Down
69 changes: 48 additions & 21 deletions store/app/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from pydantic import BaseModel

from store.app.errors import InternalError
from store.app.utils.password import hash_password
from store.settings import settings
from store.utils import new_uuid
Expand Down Expand Up @@ -201,27 +202,6 @@ def get_artifact_type(content_type: str, filename: str) -> ArtifactType:
raise ValueError(f"Unknown content type for file: {filename}")


def get_artifact_name(id: str, artifact_type: ArtifactType, size: ArtifactSize = "large") -> str:
match artifact_type:
case "image":
if size is None:
raise ValueError("Image artifacts should have a size")
height, width = SizeMapping[size]
return f"{id}_{size}_{height}x{width}.png"
case "urdf":
return f"{id}.urdf"
case "mjcf":
return f"{id}.xml"
case "stl":
return f"{id}.stl"
case _:
raise ValueError(f"Unknown artifact type: {artifact_type}")


def get_artifact_url(id: str, artifact_type: ArtifactType, size: ArtifactSize = "large") -> str:
return f"{settings.site.artifact_base_url}/{get_artifact_name(id, artifact_type, size)}"


def get_content_type(artifact_type: ArtifactType) -> str:
return DOWNLOAD_CONTENT_TYPE[artifact_type]

Expand Down Expand Up @@ -312,3 +292,50 @@ def create(cls, listing_id: str, tag: str) -> Self:
listing_id=listing_id,
name=tag,
)


def get_artifact_name(
*,
artifact: Artifact | None = None,
listing_id: str | None = None,
name: str | None = None,
artifact_type: ArtifactType | None = None,
size: ArtifactSize = "large",
) -> str:
if artifact:
listing_id = artifact.listing_id
name = artifact.name
artifact_type = artifact.artifact_type
elif not listing_id or not name or not artifact_type:
raise InternalError("Must provide artifact or listing_id, name, and artifact_type")

match artifact_type:
case "image":
height, width = SizeMapping[size]
return f"{listing_id}/{size}_{height}x{width}_{name}"
case "urdf":
return f"{listing_id}/{name}"
case "mjcf":
return f"{listing_id}/{name}"
case "stl":
return f"{listing_id}/{name}"
case _:
raise ValueError(f"Unknown artifact type: {artifact_type}")


def get_artifact_url(
*,
artifact: Artifact | None = None,
artifact_type: ArtifactType | None = None,
listing_id: str | None = None,
name: str | None = None,
size: ArtifactSize = "large",
) -> str:
artifact_name = get_artifact_name(
artifact=artifact,
listing_id=listing_id,
name=name,
artifact_type=artifact_type,
size=size,
)
return f"{settings.site.artifact_base_url}/{artifact_name}"
Loading

0 comments on commit 477e35f

Please sign in to comment.