-- Миграция 001: таблица roblox_assets для кеширования assets Roblox CDN -- -- Применяется к storys_db. -- Запускать на S2 VM 117 (service-storys, primary PG). -- При репликации S2 → S1 миграция доедет автоматически. BEGIN; CREATE TABLE IF NOT EXISTS roblox_assets ( -- ID ассета в Roblox (числовой, из rbxassetid://) rbx_asset_id BIGINT PRIMARY KEY, -- SHA256 сырого файла (после скачки с CDN, до конверта) -- Используется для второй дедупликации: разные rbx_asset_id могут указывать -- на одинаковый файл (Roblox делает редиректы). sha256_raw CHAR(64) NOT NULL, -- Тип ассета: 'mesh', 'texture', 'sound', 'csg', 'animation', 'video', 'unknown' asset_kind VARCHAR(16) NOT NULL, -- Content-Type как пришёл с CDN content_type VARCHAR(64) NOT NULL, -- Размер сырого файла raw_size_bytes BIGINT NOT NULL, -- Путь сырого файла в /opt/roblox-assets/raw/.bin (или конкретное расширение) raw_path TEXT NOT NULL, -- Если делали конверт (mesh→glb, csg→glb) — путь и хеш конвертированного файла. -- Для остальных типов = NULL. converted_path TEXT, converted_sha256 CHAR(64), converted_size_bytes BIGINT, -- URL по которому ассет реально отдаётся юзеру (https://assets.rublox.pro/...) public_url TEXT NOT NULL, -- Время первой скачки downloaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Последнее использование (бампается при каждом импорте новой карты) last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Сколько карт использует этот ассет (для cleanup'а) refcount INTEGER NOT NULL DEFAULT 0, -- Метаданные конкретного типа (mesh: vertex count, texture: dimensions) metadata JSONB DEFAULT '{}'::jsonb, -- Failed reason если скачка/конверт упал error_msg TEXT ); CREATE INDEX IF NOT EXISTS idx_roblox_assets_sha256 ON roblox_assets(sha256_raw); CREATE INDEX IF NOT EXISTS idx_roblox_assets_kind ON roblox_assets(asset_kind); CREATE INDEX IF NOT EXISTS idx_roblox_assets_last_used ON roblox_assets(last_used_at); -- Лог скачек по проектам (для отладки и tracking'а кто чем пользуется) CREATE TABLE IF NOT EXISTS roblox_asset_usage ( id BIGSERIAL PRIMARY KEY, project_id INTEGER NOT NULL, rbx_asset_id BIGINT NOT NULL REFERENCES roblox_assets(rbx_asset_id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(project_id, rbx_asset_id) ); CREATE INDEX IF NOT EXISTS idx_asset_usage_project ON roblox_asset_usage(project_id); -- Лог импортов .rbxl (родительская запись для usage) CREATE TABLE IF NOT EXISTS roblox_imports ( id BIGSERIAL PRIMARY KEY, project_id INTEGER, -- может быть NULL если ещё не создан user_id INTEGER NOT NULL, -- кто грузил (МИН только пока) rbxl_filename TEXT NOT NULL, rbxl_size BIGINT NOT NULL, rbxl_sha256 CHAR(64) NOT NULL, instance_count INTEGER, class_count INTEGER, assets_total INTEGER DEFAULT 0, assets_failed INTEGER DEFAULT 0, status VARCHAR(16) NOT NULL DEFAULT 'pending', -- pending/parsing/downloading/converting/done/failed error_msg TEXT, started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), finished_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_imports_user ON roblox_imports(user_id); CREATE INDEX IF NOT EXISTS idx_imports_status ON roblox_imports(status); COMMIT;