# frozen_string_literal: true require 'filemagic' # Define un campo de archivo class MetadataFile < MetadataTemplate # Una ruta vacía a la imagen con una descripción vacía def default_value super || { 'path' => nil, 'description' => nil } end def empty? value == default_value 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}.no_file_for_description") if no_file_for_description? errors.compact! errors.empty? 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 # Determina si la ruta es opcional pero deja pasar si la ruta se # especifica def path_optional? !required && !path? end # Asociar la imagen subida al sitio y obtener la ruta # # XXX: Si evitamos guardar cambios con changed? no tenemos forma de # saber que un archivo subido manualmente se convirtió en # un Attachment y cada vez que lo editemos vamos a subir una imagen # repetida. def save value['description'] = sanitize value['description'] if path? hardlink value['path'] = relative_destination_path else value['path'] = nil end 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 # # XXX: La última opción provoca archivos duplicados, pero es lo mejor # que tenemos hasta que resolvamos https://0xacab.org/sutty/sutty/-/issues/213 # # @return [ActiveStorage::Attachment] def static_file return unless path? @static_file ||= case value['path'] when ActionDispatch::Http::UploadedFile site.static_files.last if site.static_files.attach(value['path']) when String if (blob = ActiveStorage::Blob.where(key: key_from_path).pluck(:id).first) site.static_files.find_by(blob_id: blob) elsif site.static_files.attach(io: path.open, filename: path.basename) site.static_files.last end end end def key_from_path path.dirname.basename.to_s end def path? value['path'].present? end private def filemagic @filemagic ||= FileMagic.new(FileMagic::MAGIC_MIME) end # @return [Pathname] def path @path ||= Pathname.new(File.join(site.path, value['path'])) end def file return unless path? @file ||= case value['path'] when ActionDispatch::Http::UploadedFile then value['path'].tempfile.path when String then File.join(site.path, value['path']) end end # Hacemos un link duro para colocar el archivo dentro del repositorio # y no duplicar el espacio que ocupan. Esto requiere que ambos # directorios estén dentro del mismo punto de montaje. # # XXX: Asumimos que el archivo destino no existe porque siempre # contiene una key única. # # @return [Boolean] def hardlink return if hardlink? return if File.exist? destination_path FileUtils.mkdir_p(File.dirname(destination_path)) FileUtils.ln(uploaded_path, destination_path).zero? end def hardlink? File.stat(uploaded_path).ino == File.stat(destination_path).ino rescue Errno::ENOENT false end # Obtener la ruta al archivo # https://stackoverflow.com/a/53908358 def uploaded_relative_path ActiveStorage::Blob.service.path_for(static_file.key) end # @return [String] def uploaded_path Rails.root.join uploaded_relative_path end # La ruta del archivo mantiene el nombre original pero contiene el # nombre interno y único del archivo para poder relacionarlo con el # archivo subido en Sutty. # # @return [String] def relative_destination_path @relative_destination_path ||= File.join('public', static_file.key, static_file.filename.to_s) end # @return [String] def destination_path @destination_path ||= File.join(site.path, relative_destination_path) end # No hay archivo pero se lo describió def no_file_for_description? value['description'].present? && value['path'].blank? end end