# frozen_string_literal: true class Site # Indexa todos los artículos de un sitio # # TODO: Hacer opcional module Index extend ActiveSupport::Concern included do has_many :indexed_posts, dependent: :destroy MODIFIED_STATUSES = %i[added modified renamed].freeze LOCALE_FROM_PATH = /\A_/.freeze def blob_service @blob_service ||= ActiveStorage::Service::JekyllService.build_for_site(site: self) end def index_posts! Site.transaction do docs.each(&:index!) update(last_indexed_commit: repository.head_commit.oid) end end # Encuentra los artículos modificados entre dos commits y los # reindexa. def reindex_changes! return unless reindexable? Site.transaction do remove_deleted_posts! reindex_modified_posts! update(last_indexed_commit: repository.head_commit.oid) end end # No hacer nada si el repositorio no cambió o no hubo cambios # necesarios def reindexable? return false if last_indexed_commit.blank? return false if last_indexed_commit == repository.head_commit.oid !indexable_posts.empty? end private # Trae el último commit indexado desde el repositorio # # @return [Rugged::Commit] def indexed_commit @indexed_commit ||= repository.rugged.lookup(last_indexed_commit) end # Calcula la diferencia entre el último commit indexado y el # actual # # XXX: Esto no tiene en cuenta modificaciones en la historia como # cambio de ramas, reverts y etc, solo asume que se mueve hacia # adelante en la misma rama o las dos ramas están relacionadas. # # @return [Rugged::Diff] def diff_with_head @diff_with_head ||= indexed_commit.diff(repository.head_commit) end # Obtiene todos los archivos a reindexar # # @return [Array] def indexable_posts @indexable_posts ||= diff_with_head.each_delta.select do |delta| locales.any? do |locale| delta.old_file[:path].start_with? "_#{locale}/" end end end # Encuentra todos los archivos estáticos a reindexar, si fueron # borrados necesitamos saber la ubicación anterior, si son nuevos, # la nueva. # # @return [Array] def indexable_static_files @indexable_static_files ||= diff_with_head.each_delta.select do |delta| ( delta.status == :deleted && delta.old_file[:path].start_with?('public/') ) || delta.new_file[:path].start_with?('public/') end end # Elimina los artículos eliminados o que cambiaron de ubicación # del índice def remove_deleted_posts! indexable_posts.select(&:deleted?).each do |delta| locale, path = locale_and_path_from(delta.old_file[:path]) indexed_posts.destroy_by(locale: locale, path: path).tap do |destroyed_posts| next unless destroyed_posts.empty? Rails.logger.info I18n.t('indexed_posts.deleted', site: name, path: path, records: destroyed_posts.count) end end end # Elimina de ActiveStorage los archivos borrados, renombrados o # modificados, para poder recalcular su contenido y nueva # ubicación. # # Los renombrados o modificados se vuelven a crear en # reindex_modified_static_files! def remove_deleted_or_renamed_static_files! indexable_static_files.select do |delta| delta.deleted? || delta.modified? || delta.renamed? end.each do |delta| key = blob_service.key_from_path(delta.old_file[:path]) static_files.blobs.find_by(service_name: name, key: key).tap do |blob| next unless blob transaction do static_files.where(blob_id: blob.id).destroy_all blob.destroy end end end end # Reindexa los archivos estáticos modificados def reindex_modified_static_files! indexable_static_files.select do |delta| MODIFIED_STATUSES.include?(delta.status) || delta.renamed? end.each do |delta| transaction do path = blob_service.absolute_path_for(delta.new_file[:path].sub(%r{\Apublic/}, '')) key = blob_service.key_from_path(path) pathname = Pathname.new(path) blob = ActiveStorage::Blob.create_after_unfurling!(key: key, io: pathname.open, filename: pathname.basename, service_name: name) ActiveStorage::Attachment.create!(name: 'static_files', record: self, blob: blob) end end end # Reindexa artículos que cambiaron de ubicación, se agregaron # o fueron modificados def reindex_modified_posts! indexable_posts.select do |delta| MODIFIED_STATUSES.include? delta.status end.each do |delta| locale, path = locale_and_path_from(delta.new_file[:path]) posts(lang: locale).find(path).index! end end # Obtiene el idioma y la ruta del post a partir de la ubicación en # el disco. # # Las rutas vienen en ASCII-9BIT desde Rugged, pero en realidad # son UTF-8 # # @return [Array] def locale_and_path_from(path) locale, path = path.force_encoding('utf-8').split(File::SEPARATOR, 2) [ locale.sub(LOCALE_FROM_PATH, ''), File.basename(path, '.*') ] end end end end