-- ============================================ -- Storage Files Table -- Main table for tracking uploaded files -- ============================================ CREATE TABLE storage.files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, uploaded_by UUID NOT NULL REFERENCES users.users(id) ON DELETE SET NULL, -- File info filename VARCHAR(255) NOT NULL, original_name VARCHAR(500) NOT NULL, mime_type VARCHAR(100) NOT NULL, size_bytes BIGINT NOT NULL CHECK (size_bytes > 0), -- Storage location bucket VARCHAR(100) NOT NULL, path VARCHAR(1000) NOT NULL, provider storage.storage_provider NOT NULL DEFAULT 's3', -- Status status storage.file_status NOT NULL DEFAULT 'ready', visibility storage.visibility NOT NULL DEFAULT 'private', -- Metadata metadata JSONB DEFAULT '{}', thumbnails JSONB DEFAULT '{}', -- Folder organization folder VARCHAR(100) DEFAULT 'files', -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, -- Unique constraint on path within bucket CONSTRAINT uq_storage_files_path UNIQUE (bucket, path) ); -- Indexes CREATE INDEX idx_storage_files_tenant ON storage.files(tenant_id); CREATE INDEX idx_storage_files_tenant_folder ON storage.files(tenant_id, folder); CREATE INDEX idx_storage_files_uploaded_by ON storage.files(uploaded_by); CREATE INDEX idx_storage_files_mime_type ON storage.files(mime_type); CREATE INDEX idx_storage_files_status ON storage.files(status); CREATE INDEX idx_storage_files_created_at ON storage.files(created_at DESC); CREATE INDEX idx_storage_files_deleted ON storage.files(tenant_id, deleted_at) WHERE deleted_at IS NULL; -- RLS ALTER TABLE storage.files ENABLE ROW LEVEL SECURITY; CREATE POLICY storage_files_tenant_isolation ON storage.files FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- Comments COMMENT ON TABLE storage.files IS 'Uploaded files metadata and storage location'; COMMENT ON COLUMN storage.files.path IS 'Full path in bucket: tenant_id/folder/upload_id/filename'; COMMENT ON COLUMN storage.files.thumbnails IS 'Generated thumbnail paths as JSON {thumb: path, medium: path}'; COMMENT ON COLUMN storage.files.folder IS 'Logical folder: avatars, documents, imports, etc.'; -- ============================================ -- Pending Uploads Table -- Tracks upload requests before confirmation -- ============================================ CREATE TABLE storage.pending_uploads ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, -- Upload request info filename VARCHAR(255) NOT NULL, original_name VARCHAR(500) NOT NULL, mime_type VARCHAR(100) NOT NULL, size_bytes BIGINT NOT NULL, folder VARCHAR(100) DEFAULT 'files', -- Presigned URL info bucket VARCHAR(100) NOT NULL, path VARCHAR(1000) NOT NULL, provider storage.storage_provider NOT NULL DEFAULT 's3', -- Status status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'completed', 'expired', 'failed')), expires_at TIMESTAMPTZ NOT NULL, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ ); -- Indexes CREATE INDEX idx_storage_pending_tenant ON storage.pending_uploads(tenant_id); CREATE INDEX idx_storage_pending_user ON storage.pending_uploads(user_id); CREATE INDEX idx_storage_pending_status ON storage.pending_uploads(status); CREATE INDEX idx_storage_pending_expires ON storage.pending_uploads(expires_at) WHERE status = 'pending'; -- RLS ALTER TABLE storage.pending_uploads ENABLE ROW LEVEL SECURITY; CREATE POLICY storage_pending_tenant_isolation ON storage.pending_uploads FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- Auto-cleanup expired uploads (can be called by cron) CREATE OR REPLACE FUNCTION storage.cleanup_expired_uploads() RETURNS INTEGER AS $$ DECLARE deleted_count INTEGER; BEGIN UPDATE storage.pending_uploads SET status = 'expired' WHERE status = 'pending' AND expires_at < NOW(); GET DIAGNOSTICS deleted_count = ROW_COUNT; RETURN deleted_count; END; $$ LANGUAGE plpgsql; COMMENT ON TABLE storage.pending_uploads IS 'Pending file uploads awaiting confirmation'; COMMENT ON FUNCTION storage.cleanup_expired_uploads IS 'Mark expired pending uploads';