# frozen_string_literal: true require 'filemagic' # Define un campo de archivo class MetadataFile < MetadataTemplate include Metadata::NonIndexableConcern include Metadata::AlwaysPublicConcern # Una ruta vacía a la imagen con una descripción vacía def default_value super || { 'path' => nil, 'description' => '' } end # La descripción es opcional # # @return [Boolean] def empty? value.nil? || value['path'].blank? end # No hay valores sugeridos para archivos subidos. def values raise NotImplementedError, "#{self.class} no tiene valores sugeridos" end def validate super errors << I18n.t("metadata.#{type}.site_invalid") if site.invalid? errors << I18n.t("metadata.#{type}.path_required") if path_missing? errors << I18n.t("metadata.#{type}.attachment_missing") if path? && !static_file errors.compact! errors.empty? end # Asociar la imagen subida al sitio y obtener la ruta # @return [Boolean] def save return true unless changed? self[:value] = default_value if empty? self[:value] = sanitize(value) value['path'] = relative_destination_path_with_filename.to_s if static_file true end # Almacena el archivo en el sitio y lo devuelve o lo obtiene de la # base de datos. # # Existen tres casos: # # * El archivo fue subido a través de HTTP # * El archivo es una ruta que apunta a un archivo asociado al sitio # * El archivo es una ruta a un archivo dentro del repositorio # # @todo encontrar una forma de obtener el attachment sin tener que # recurrir al último subido. # # @return [ActiveStorage::Attachment,nil] def static_file @static_file ||= case value['path'] when ActionDispatch::Http::UploadedFile site.static_files.last if site.static_files.attach(value['path']) when String site.static_files.find_by(blob_id: blob_id) || migrate_static_file! end end private # Valida que estemos pasando el formato correcto # # @param :value [Any] # @return [Hash,nil] def sanitize(value) return unless value.is_a? Hash value.dup.tap do |v| v['description'] = super(v['description']) end end # Obtener la ruta al archivo relativa al sitio # # @return [Pathname] def destination_path Pathname.new(static_file_path) end # Agrega el nombre de archivo a la ruta para tener retrocompatibilidad # # @return [Pathname] def destination_path_with_filename destination_path.realpath # Si el archivo no llegara a existir, en lugar de hacer fallar todo, # devolvemos la ruta original, que puede ser el archivo que no existe # o vacía si se está subiendo uno. rescue Errno::ENOENT => e ExceptionNotifier.notify_exception(e, data: { site: site.name, path: value['path'] }) Pathname.new(File.join(site.path, value['path'])) end # Obtener la ruta relativa al sitio. # # Si algo falla, devolver la ruta original para no romper el archivo. # # @return [String, nil] def relative_destination_path_with_filename destination_path_with_filename.relative_path_from(Pathname.new(site.path).realpath) rescue ArgumentError => e ExceptionNotifier.notify_exception(e, data: { site: site.name, path: value['path'] }) value['path'] end # La ruta absoluta al archivo # # @todo Eliminar retrocompatibilidad # @return [String] def static_file_path case static_file.blob.service.name when :local File.join(site.path, 'public', static_file.key, static_file.filename.to_s) else static_file.blob.service.path_for(static_file.key) end end # Obtiene el id del blob asociado # # @return [Integer,nil] def blob_id @blob_id ||= ActiveStorage::Blob.where(key: key_from_path, service_name: site.name).pluck(:id).first end # Genera el blob para un archivo que ya se encuentra en el # repositorio y lo agrega a la base de datos. # # @return [ActiveStorage::Attachment] def migrate_static_file! raise ArgumentError, 'El archivo no existe' unless path? && pathname.exist? Site.transaction do blob = ActiveStorage::Blob.create_after_unfurling!(key: key_from_path, io: pathname.open, filename: pathname.basename, service_name: site.name) ActiveStorage::Attachment.create!(name: 'static_files', record: site, blob: blob) end rescue ArgumentError => e ExceptionNotifier.notify_exception(e, data: { site: site.name, path: value['path'] }) nil end # Determina si necesitamos la imagen pero no la tenemos def path_missing? required && !path? end # Determina si el archivo ya fue subido def uploaded? value['path'].is_a?(String) end # Obtiene la ruta absoluta al archivo # # @return [Pathname] def pathname raise NoMethodError unless uploaded? @pathname ||= Pathname.new(File.join(site.path, value['path'])) end # Obtiene la key del attachment a partir de la ruta # # @return [String] def key_from_path @key_from_path ||= pathname.dirname.basename.to_s end # @todo Este método no se puede correr sobre archivos recién subidos def path? value['path'].present? end def description? value['description'].present? end end