-- ===================================================================== -- FILE_MGR SCHEMA — File Manager, Storage Quotas, Cleanup Scheduler -- ===================================================================== SET search_path TO file_mgr, public; CREATE TYPE file_kind AS ENUM ('Video','Image','Audio','Voiceover','Document','Other'); CREATE TYPE folder_kind AS ENUM ('System','User','Shared','Tenant'); CREATE TYPE upload_status AS ENUM ('Pending','Uploading','Processing','Ready','Failed','Quarantined'); CREATE TYPE cleanup_entity_type AS ENUM ('Export','TempRenderFolder','OrphanedFile','UnusedUpload','SnapshotExpired'); CREATE TYPE cleanup_status AS ENUM ('Scheduled','Notified','Processing','Done','Skipped','Failed'); -- --------------------------------------------------------------------- -- user_folders — hierarchical folders -- --------------------------------------------------------------------- CREATE TABLE user_folders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, user_id UUID NOT NULL, name TEXT NOT NULL, folder_type folder_kind NOT NULL DEFAULT 'User', parent_folder_id UUID REFERENCES user_folders(id) ON DELETE CASCADE, -- Stats (denormalized for fast UI) file_count INT NOT NULL DEFAULT 0, total_size_bytes BIGINT NOT NULL DEFAULT 0, sort INT NOT NULL DEFAULT 0, is_shared BOOLEAN NOT NULL DEFAULT FALSE, share_token TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ ); CREATE INDEX idx_folders_user ON user_folders(user_id, parent_folder_id) WHERE deleted_at IS NULL; CREATE INDEX idx_folders_parent ON user_folders(parent_folder_id) WHERE deleted_at IS NULL; CREATE INDEX idx_folders_share ON user_folders(share_token) WHERE share_token IS NOT NULL; CREATE TRIGGER tg_folders_updated_at BEFORE UPDATE ON user_folders FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); -- --------------------------------------------------------------------- -- user_files -- --------------------------------------------------------------------- CREATE TABLE user_files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, user_id UUID NOT NULL, user_folder_id UUID REFERENCES user_folders(id) ON DELETE SET NULL, -- Identity name TEXT NOT NULL, original_filename TEXT, file_extension TEXT, mime_type TEXT, file_type file_kind NOT NULL, -- Storage minio_bucket TEXT NOT NULL, minio_key TEXT NOT NULL, cdn_url TEXT, file_address TEXT NOT NULL, -- canonical URL size_bytes BIGINT NOT NULL, md5_hash TEXT, sha256_hash TEXT, -- Media metadata duration_sec NUMERIC(8,2), width INT, height INT, fps NUMERIC(5,2), bitrate_kbps INT, codec TEXT, has_audio BOOLEAN, has_video BOOLEAN, -- Thumbnails thumbnail_url TEXT, waveform_data JSONB, -- for audio files -- Upload state upload_status upload_status NOT NULL DEFAULT 'Ready', upload_id TEXT, -- multipart upload ID if used upload_progress INT NOT NULL DEFAULT 100, processing_error TEXT, -- Source / linkage source TEXT, -- 'upload','export','snapshot','voiceover_record','stock' export_id UUID, -- references render.exports parent_file_id UUID REFERENCES user_files(id) ON DELETE SET NULL, -- derived files -- Lifecycle last_used_at TIMESTAMPTZ, use_count INT NOT NULL DEFAULT 0, -- Sharing is_public BOOLEAN NOT NULL DEFAULT FALSE, share_token TEXT, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ ); CREATE INDEX idx_files_user_folder ON user_files(user_id, user_folder_id, created_at DESC) WHERE deleted_at IS NULL; CREATE INDEX idx_files_tenant ON user_files(tenant_id) WHERE deleted_at IS NULL; CREATE INDEX idx_files_type ON user_files(user_id, file_type) WHERE deleted_at IS NULL; CREATE INDEX idx_files_hash ON user_files(md5_hash) WHERE md5_hash IS NOT NULL; CREATE INDEX idx_files_unused ON user_files(last_used_at) WHERE deleted_at IS NULL; CREATE INDEX idx_files_name_trgm ON user_files USING gin (name gin_trgm_ops); CREATE INDEX idx_files_share ON user_files(share_token) WHERE share_token IS NOT NULL; CREATE TRIGGER tg_files_updated_at BEFORE UPDATE ON user_files FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); -- --------------------------------------------------------------------- -- storage_quotas — current usage per user -- --------------------------------------------------------------------- CREATE TABLE storage_quotas ( user_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, plan_quota_bytes BIGINT NOT NULL DEFAULT 0, -- from plan bonus_quota_bytes BIGINT NOT NULL DEFAULT 0, -- purchased extra used_bytes BIGINT NOT NULL DEFAULT 0, -- Cached counts video_count INT NOT NULL DEFAULT 0, image_count INT NOT NULL DEFAULT 0, audio_count INT NOT NULL DEFAULT 0, video_bytes BIGINT NOT NULL DEFAULT 0, image_bytes BIGINT NOT NULL DEFAULT 0, audio_bytes BIGINT NOT NULL DEFAULT 0, -- Notifications last_90pct_notified_at TIMESTAMPTZ, last_100pct_notified_at TIMESTAMPTZ, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_quotas_tenant ON storage_quotas(tenant_id); CREATE TRIGGER tg_quotas_updated_at BEFORE UPDATE ON storage_quotas FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); -- --------------------------------------------------------------------- -- cleanup_schedules — track what's queued for auto-delete -- --------------------------------------------------------------------- CREATE TABLE cleanup_schedules ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID, user_id UUID, entity_type cleanup_entity_type NOT NULL, entity_id UUID NOT NULL, entity_path TEXT, -- filesystem path for temp folders scheduled_delete_at TIMESTAMPTZ NOT NULL, notify_user_at TIMESTAMPTZ, -- send "expires in 3 days" notice user_notified BOOLEAN NOT NULL DEFAULT FALSE, user_notified_at TIMESTAMPTZ, status cleanup_status NOT NULL DEFAULT 'Scheduled', processed_at TIMESTAMPTZ, processing_error TEXT, bytes_freed BIGINT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_cleanup_due ON cleanup_schedules(scheduled_delete_at) WHERE status = 'Scheduled'; CREATE INDEX idx_cleanup_notify_due ON cleanup_schedules(notify_user_at) WHERE user_notified = FALSE AND notify_user_at IS NOT NULL; CREATE INDEX idx_cleanup_entity ON cleanup_schedules(entity_type, entity_id); CREATE TRIGGER tg_cleanup_updated_at BEFORE UPDATE ON cleanup_schedules FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); -- --------------------------------------------------------------------- -- upload_sessions — multipart / chunked upload tracking -- --------------------------------------------------------------------- CREATE TABLE upload_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, user_id UUID NOT NULL, minio_bucket TEXT NOT NULL, minio_key TEXT NOT NULL, minio_upload_id TEXT NOT NULL, -- S3/MinIO multipart ID filename TEXT NOT NULL, mime_type TEXT, total_size_bytes BIGINT NOT NULL, chunks_received INT NOT NULL DEFAULT 0, bytes_received BIGINT NOT NULL DEFAULT 0, chunk_size_bytes INT NOT NULL DEFAULT 5242880, -- 5MB default target_folder_id UUID REFERENCES user_folders(id) ON DELETE SET NULL, target_file_id UUID, -- created when complete status upload_status NOT NULL DEFAULT 'Uploading', error_message TEXT, expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours', completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_uploads_user ON upload_sessions(user_id, created_at DESC); CREATE INDEX idx_uploads_expired ON upload_sessions(expires_at) WHERE status = 'Uploading'; CREATE TRIGGER tg_uploads_updated_at BEFORE UPDATE ON upload_sessions FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); -- --------------------------------------------------------------------- -- minio_buckets — bucket registry (per region, per purpose) -- --------------------------------------------------------------------- CREATE TABLE minio_buckets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL UNIQUE, region TEXT NOT NULL, endpoint TEXT NOT NULL, purpose TEXT NOT NULL, -- 'templates','user-uploads','exports','snapshots','voiceovers' is_public BOOLEAN NOT NULL DEFAULT FALSE, cdn_base_url TEXT, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_buckets_region_purpose ON minio_buckets(region, purpose) WHERE is_active = TRUE;