Files
openstack-api-simulator/app/openstack/routes/swift.py
T

215 lines
6.6 KiB
Python

"""Swift Object Storage API v1."""
from __future__ import annotations
import json
from typing import Annotated
from uuid import uuid4
from asyncpg import Connection
from fastapi import APIRouter, Depends, Request, Response
from app.openstack.auth import TokenContext
from app.openstack.deps import get_conn, require_token
from app.openstack.errors import OpenStackError
router = APIRouter(tags=["Swift"])
def _account(ctx: TokenContext) -> str:
return f"AUTH_{ctx.project_id or ctx.user_id}"
@router.get("/info")
async def swift_info(conn: Annotated[Connection, Depends(get_conn)]) -> dict[str, object]:
from app.openstack.db_docs import require_doc
return await require_doc(conn, service="swift", resource_type="info", name="default")
@router.get("/v1/{account}")
async def list_containers(
account: str,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_token)],
) -> list[dict[str, object]]:
_ = account
rows = await conn.fetch(
"SELECT name, meta, created_at FROM os_swift_containers WHERE account=$1 ORDER BY name",
_account(ctx),
)
result = []
for r in rows:
count = await conn.fetchval(
"SELECT count(*) FROM os_swift_objects WHERE account=$1 AND container=$2",
_account(ctx),
r["name"],
)
bytes_total = await conn.fetchval(
"SELECT COALESCE(sum(bytes),0) FROM os_swift_objects WHERE account=$1 AND container=$2",
_account(ctx),
r["name"],
)
result.append({"name": r["name"], "count": int(count or 0), "bytes": int(bytes_total or 0)})
return result
@router.put("/v1/{account}/{container}", status_code=201)
async def create_container(
account: str,
container: str,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_token)],
) -> Response:
_ = account
await conn.execute(
"""INSERT INTO os_swift_containers(account, name, meta)
VALUES($1,$2,'{}'::jsonb) ON CONFLICT DO NOTHING""",
_account(ctx),
container,
)
return Response(status_code=201)
@router.delete("/v1/{account}/{container}", status_code=204)
async def delete_container(
account: str,
container: str,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_token)],
) -> Response:
_ = account
acct = _account(ctx)
objects = await conn.fetchval(
"SELECT count(*) FROM os_swift_objects WHERE account=$1 AND container=$2",
acct,
container,
)
if int(objects or 0) > 0:
raise OpenStackError(
"Conflict",
"Container is not empty",
status_code=409,
)
result = await conn.execute(
"DELETE FROM os_swift_containers WHERE account=$1 AND name=$2",
acct,
container,
)
if result.endswith("0"):
raise OpenStackError("NotFound", "Container not found", status_code=404)
return Response(status_code=204)
@router.get("/v1/{account}/{container}")
async def list_objects(
account: str,
container: str,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_token)],
) -> list[dict[str, object]]:
_ = account
rows = await conn.fetch(
"""SELECT name, bytes, content_type, created_at FROM os_swift_objects
WHERE account=$1 AND container=$2 ORDER BY name""",
_account(ctx),
container,
)
return [
{
"name": r["name"],
"bytes": r["bytes"],
"content_type": r["content_type"],
"last_modified": r["created_at"].strftime("%Y-%m-%dT%H:%M:%S.%f"),
"hash": "0",
}
for r in rows
]
@router.put("/v1/{account}/{container}/{object_name:path}", status_code=201)
async def put_object(
account: str,
container: str,
object_name: str,
request: Request,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_token)],
) -> Response:
_ = account
body = await request.body()
await conn.execute(
"""INSERT INTO os_swift_containers(account, name, meta)
VALUES($1,$2,'{}'::jsonb) ON CONFLICT DO NOTHING""",
_account(ctx),
container,
)
await conn.execute(
"""INSERT INTO os_swift_objects(id, account, container, name, content_type, bytes, body, meta)
VALUES($1,$2,$3,$4,$5,$6,$7,'{}'::jsonb)
ON CONFLICT (account, container, name) DO UPDATE
SET bytes=EXCLUDED.bytes, body=EXCLUDED.body, content_type=EXCLUDED.content_type""",
uuid4(),
_account(ctx),
container,
object_name,
request.headers.get("content-type") or "application/octet-stream",
len(body),
body,
)
return Response(status_code=201, headers={"Etag": "0"})
@router.post("/v1/{account}/{container}/{object_name:path}", status_code=202)
async def post_object(
account: str,
container: str,
object_name: str,
request: Request,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_token)],
) -> Response:
# Metadata update / create — reuse PUT semantics
return await put_object(account, container, object_name, request, conn, ctx)
@router.get("/v1/{account}/{container}/{object_name:path}")
async def get_object(
account: str,
container: str,
object_name: str,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_token)],
) -> Response:
_ = account
row = await conn.fetchrow(
"""SELECT body, content_type FROM os_swift_objects
WHERE account=$1 AND container=$2 AND name=$3""",
_account(ctx),
container,
object_name,
)
if row is None:
raise OpenStackError("NotFound", "Object not found", status_code=404)
return Response(content=bytes(row["body"] or b""), media_type=row["content_type"])
@router.delete("/v1/{account}/{container}/{object_name:path}", status_code=204)
async def delete_object(
account: str,
container: str,
object_name: str,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_token)],
) -> Response:
_ = account
result = await conn.execute(
"DELETE FROM os_swift_objects WHERE account=$1 AND container=$2 AND name=$3",
_account(ctx),
container,
object_name,
)
if result.endswith("0"):
raise OpenStackError("NotFound", "Object not found", status_code=404)
return Response(status_code=204)