-- ============================================================================= -- MICHANGARRITO - 17 STORAGE (MCH-029: Infraestructura SaaS) -- ============================================================================= -- Sistema de almacenamiento de archivos multi-tenant -- Integra con: S3, Cloudflare R2, o almacenamiento local -- ============================================================================= -- Buckets de almacenamiento CREATE TABLE IF NOT EXISTS storage.buckets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL UNIQUE, description TEXT, -- Configuracion public BOOLEAN DEFAULT false, file_size_limit INTEGER, -- bytes, null = sin limite allowed_mime_types TEXT[], -- Metadata created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TRIGGER update_storage_buckets_updated_at BEFORE UPDATE ON storage.buckets FOR EACH ROW EXECUTE FUNCTION update_updated_at(); COMMENT ON TABLE storage.buckets IS 'Buckets de almacenamiento (contenedores)'; COMMENT ON COLUMN storage.buckets.file_size_limit IS 'Limite de tamaƱo por archivo en bytes'; COMMENT ON COLUMN storage.buckets.allowed_mime_types IS 'Array de MIME types permitidos'; -- Archivos almacenados CREATE TABLE IF NOT EXISTS storage.files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, bucket_id UUID NOT NULL REFERENCES storage.buckets(id) ON DELETE CASCADE, -- Identificacion name VARCHAR(255) NOT NULL, path VARCHAR(500) NOT NULL, -- Propiedades del archivo mime_type VARCHAR(100), size INTEGER NOT NULL, checksum VARCHAR(64), -- SHA-256 -- Metadata adicional metadata JSONB DEFAULT '{}', -- Control de acceso is_public BOOLEAN DEFAULT false, -- Referencias uploaded_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), -- Constraints UNIQUE(bucket_id, path) ); CREATE INDEX idx_storage_files_tenant ON storage.files(tenant_id); CREATE INDEX idx_storage_files_bucket ON storage.files(bucket_id); CREATE INDEX idx_storage_files_path ON storage.files(path); CREATE INDEX idx_storage_files_mime ON storage.files(mime_type); CREATE TRIGGER update_storage_files_updated_at BEFORE UPDATE ON storage.files FOR EACH ROW EXECUTE FUNCTION update_updated_at(); COMMENT ON TABLE storage.files IS 'Archivos almacenados por tenant'; COMMENT ON COLUMN storage.files.path IS 'Ruta completa del archivo: tenant_id/bucket/filename'; COMMENT ON COLUMN storage.files.metadata IS 'Metadata adicional: {width, height, duration, etc.}'; -- URLs firmadas (para acceso temporal) CREATE TABLE IF NOT EXISTS storage.signed_urls ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), file_id UUID NOT NULL REFERENCES storage.files(id) ON DELETE CASCADE, -- Token y expiracion token VARCHAR(255) NOT NULL UNIQUE, expires_at TIMESTAMPTZ NOT NULL, -- Restricciones max_downloads INTEGER, downloads_count INTEGER DEFAULT 0, -- Metadata created_at TIMESTAMPTZ DEFAULT NOW(), created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL ); CREATE INDEX idx_storage_signed_urls_token ON storage.signed_urls(token); CREATE INDEX idx_storage_signed_urls_expires ON storage.signed_urls(expires_at); COMMENT ON TABLE storage.signed_urls IS 'URLs firmadas para acceso temporal a archivos privados'; -- ============================================================================= -- SEEDS: Buckets por defecto -- ============================================================================= INSERT INTO storage.buckets (name, description, public, file_size_limit, allowed_mime_types) VALUES ('products', 'Imagenes de productos', true, 5242880, ARRAY['image/jpeg', 'image/png', 'image/webp']), ('avatars', 'Fotos de perfil de usuarios', true, 1048576, ARRAY['image/jpeg', 'image/png']), ('documents', 'Documentos privados', false, 10485760, ARRAY['application/pdf', 'image/jpeg', 'image/png']), ('exports', 'Archivos de exportacion (reportes)', false, 52428800, ARRAY['application/pdf', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/csv']), ('backups', 'Backups de datos', false, NULL, NULL) ON CONFLICT (name) DO NOTHING;