feat(E): add MediaAsset catalog — model, CRUD API, MediaBrowser UI
Migration 040: media_assets table with RLS (tenant_isolation + admin_bypass). domains/media/: MediaAsset model, schemas, service, router with ZIP-download. publish_asset Celery task in rendering/tasks.py. core/storage.py: download_bytes() method for MinIO + LocalStorage. frontend: MediaBrowser.tsx (grid/list, multi-select, zip-download, pagination) + api/media.ts. Route /media (AdminRoute) + sidebar link with Image icon for admin+pm. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""Add media_assets table.
|
||||
|
||||
Revision ID: 040
|
||||
Revises: 039
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
|
||||
revision = '040'
|
||||
down_revision = '039'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Create enum type
|
||||
op.execute(
|
||||
"CREATE TYPE media_asset_type AS ENUM ("
|
||||
"'thumbnail','still','turntable','stl_low','stl_high',"
|
||||
"'gltf_geometry','gltf_production','blend_production')"
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
'media_assets',
|
||||
sa.Column('id', UUID(as_uuid=True), primary_key=True, server_default=sa.text('gen_random_uuid()')),
|
||||
sa.Column('tenant_id', UUID(as_uuid=True), sa.ForeignKey('tenants.id', ondelete='CASCADE'), nullable=True),
|
||||
sa.Column('product_id', UUID(as_uuid=True), sa.ForeignKey('products.id', ondelete='CASCADE'), nullable=True),
|
||||
sa.Column('cad_file_id', UUID(as_uuid=True), sa.ForeignKey('cad_files.id', ondelete='SET NULL'), nullable=True),
|
||||
sa.Column('order_line_id', UUID(as_uuid=True), sa.ForeignKey('order_lines.id', ondelete='SET NULL'), nullable=True),
|
||||
sa.Column('workflow_run_id', UUID(as_uuid=True), sa.ForeignKey('workflow_runs.id', ondelete='SET NULL'), nullable=True),
|
||||
sa.Column(
|
||||
'asset_type',
|
||||
sa.Enum(
|
||||
'thumbnail', 'still', 'turntable',
|
||||
'stl_low', 'stl_high',
|
||||
'gltf_geometry', 'gltf_production', 'blend_production',
|
||||
name='media_asset_type',
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column('storage_key', sa.Text, nullable=False),
|
||||
sa.Column('file_size_bytes', sa.BigInteger, nullable=True),
|
||||
sa.Column('mime_type', sa.String(100), nullable=True),
|
||||
sa.Column('width', sa.Integer, nullable=True),
|
||||
sa.Column('height', sa.Integer, nullable=True),
|
||||
sa.Column('duration_s', sa.Float, nullable=True),
|
||||
sa.Column('render_config', JSONB, nullable=True),
|
||||
sa.Column('is_archived', sa.Boolean, nullable=False, server_default='false'),
|
||||
sa.Column('created_at', sa.DateTime, nullable=False, server_default=sa.text('NOW()')),
|
||||
)
|
||||
op.create_index('ix_media_assets_product', 'media_assets', ['product_id'])
|
||||
op.create_index('ix_media_assets_tenant', 'media_assets', ['tenant_id'])
|
||||
op.create_index('ix_media_assets_order_line', 'media_assets', ['order_line_id'])
|
||||
op.create_index('ix_media_assets_asset_type', 'media_assets', ['asset_type'])
|
||||
|
||||
# RLS
|
||||
op.execute("ALTER TABLE media_assets ENABLE ROW LEVEL SECURITY")
|
||||
op.execute(
|
||||
"""CREATE POLICY tenant_isolation ON media_assets
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid)"""
|
||||
)
|
||||
op.execute(
|
||||
"""CREATE POLICY admin_bypass ON media_assets
|
||||
USING (current_setting('app.current_tenant_id', true) = 'bypass')"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('media_assets')
|
||||
op.execute("DROP TYPE IF EXISTS media_asset_type")
|
||||
Reference in New Issue
Block a user