c74e118b98
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>
72 lines
2.9 KiB
Python
72 lines
2.9 KiB
Python
"""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")
|