diff --git a/backend/alembic/versions/040_media_assets.py b/backend/alembic/versions/040_media_assets.py index 476ecba..216d3da 100644 --- a/backend/alembic/versions/040_media_assets.py +++ b/backend/alembic/versions/040_media_assets.py @@ -14,56 +14,58 @@ 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')" - ) + # Use raw SQL throughout for full idempotency (enum may exist from a partial run) + op.execute(""" + DO $$ BEGIN + CREATE TYPE media_asset_type AS ENUM ( + 'thumbnail','still','turntable','stl_low','stl_high', + 'gltf_geometry','gltf_production','blend_production' + ); + EXCEPTION WHEN duplicate_object THEN NULL; + END $$; + """) - 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']) + op.execute(""" + CREATE TABLE IF NOT EXISTS media_assets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, + product_id UUID REFERENCES products(id) ON DELETE CASCADE, + cad_file_id UUID REFERENCES cad_files(id) ON DELETE SET NULL, + order_line_id UUID REFERENCES order_lines(id) ON DELETE SET NULL, + workflow_run_id UUID REFERENCES workflow_runs(id) ON DELETE SET NULL, + asset_type media_asset_type NOT NULL, + storage_key TEXT NOT NULL, + file_size_bytes BIGINT, + mime_type VARCHAR(100), + width INTEGER, + height INTEGER, + duration_s FLOAT, + render_config JSONB, + is_archived BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMP NOT NULL DEFAULT NOW() + ) + """) + + op.execute("CREATE INDEX IF NOT EXISTS ix_media_assets_product ON media_assets (product_id)") + op.execute("CREATE INDEX IF NOT EXISTS ix_media_assets_tenant ON media_assets (tenant_id)") + op.execute("CREATE INDEX IF NOT EXISTS ix_media_assets_order_line ON media_assets (order_line_id)") + op.execute("CREATE INDEX IF NOT EXISTS ix_media_assets_asset_type ON 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')""" - ) + op.execute(""" + DO $$ BEGIN + CREATE POLICY tenant_isolation ON media_assets + USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); + EXCEPTION WHEN duplicate_object THEN NULL; + END $$; + """) + op.execute(""" + DO $$ BEGIN + CREATE POLICY admin_bypass ON media_assets + USING (current_setting('app.current_tenant_id', true) = 'bypass'); + EXCEPTION WHEN duplicate_object THEN NULL; + END $$; + """) def downgrade(): diff --git a/docker-compose.yml b/docker-compose.yml index f1fa241..117d7eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -114,8 +114,8 @@ services: render-worker: build: - context: ./render-worker - dockerfile: Dockerfile + context: . + dockerfile: render-worker/Dockerfile args: - BLENDER_VERSION=${BLENDER_VERSION:-5.0.1} environment: diff --git a/render-worker/Dockerfile b/render-worker/Dockerfile index 3bae690..608a440 100644 --- a/render-worker/Dockerfile +++ b/render-worker/Dockerfile @@ -11,9 +11,17 @@ ENV PYOPENGL_PLATFORM=osmesa ENV VTK_DEFAULT_EGL=0 # Runtime libraries for cadquery/OCC + Blender 5.x headless +# Also installs Python 3.11 via deadsnakes PPA (Ubuntu 22.04 ships 3.10) RUN apt-get update && apt-get install -y --no-install-recommends \ + software-properties-common gnupg gpg-agent \ + && add-apt-repository ppa:deadsnakes/ppa -y \ + && apt-get update && apt-get install -y --no-install-recommends \ + python3.11 \ + python3.11-dev \ + python3.11-distutils \ python3-pip \ python3-dev \ + curl \ libpq-dev \ gcc \ libxrender1 \ @@ -40,21 +48,26 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /app +# Use Python 3.11 as the default pip target +RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11 \ + && update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 \ + && update-alternatives --install /usr/bin/pip3 pip3 /usr/local/bin/pip3.11 1 + # Install backend Python dependencies (includes celery, sqlalchemy, fastapi, etc.) -COPY pyproject.toml . +COPY backend/pyproject.toml . RUN pip3 install --no-cache-dir -e . # Install cadquery (heavy — installed after backend deps for better layer caching) RUN pip3 install --no-cache-dir "cadquery>=2.4.0" # Copy render scripts -COPY scripts/ /render-scripts/ +COPY render-worker/scripts/ /render-scripts/ # Version check script — fails fast if Blender < 5.0.1 -COPY check_version.py /check_version.py +COPY render-worker/check_version.py /check_version.py -# Copy app code (overridden by volume mount in docker-compose) -COPY . . +# Copy backend app code (overridden by volume mount in docker-compose) +COPY backend/ . # Verify Blender version at build time if binary is available # (skipped during build since /opt/blender is a host mount)