From 11b347e4c06ef06c064d2f8726c494dda3a3c280 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 14 Nov 2019 14:27:24 -0300 Subject: [PATCH] =?UTF-8?q?migraci=C3=B3n=20de=20archivos=20est=C3=A1ticos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/metadata_file.rb | 14 ++- app/models/post.rb | 4 +- app/models/site.rb | 7 +- app/models/site/static_file_migration.rb | 124 +++++++++++++++++++++++ config/initializers/analyze_job.rb | 7 ++ config/locales/en.yml | 1 + config/locales/es.yml | 1 + doc/static_files.md | 30 ++++++ test/models/post_test.rb | 6 ++ 9 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 app/models/site/static_file_migration.rb create mode 100644 config/initializers/analyze_job.rb create mode 100644 doc/static_files.md diff --git a/app/models/metadata_file.rb b/app/models/metadata_file.rb index 06e69be0..efbd2e07 100644 --- a/app/models/metadata_file.rb +++ b/app/models/metadata_file.rb @@ -56,6 +56,8 @@ class MetadataFile < MetadataTemplate # # @return ActiveStorage::Attachment def static_file + return @static_file if @static_file + ActiveRecord::Base.connection_pool.with_connection do if uploaded? blob = ActiveStorage::Blob.find_by(key: key_from_path) @@ -68,10 +70,12 @@ class MetadataFile < MetadataTemplate private + def path + @path ||= Pathname.new value['path'] + end + def key_from_path - # XXX: No podemos usar self#extension porque en este punto todavía - # no sabemos el static_file - File.basename(value['path'], '.*') + path.dirname.basename.to_s end # Hacemos un link duro para colocar el archivo dentro del repositorio @@ -83,7 +87,7 @@ class MetadataFile < MetadataTemplate end def extension - static_file.blob.content_type.split('/').last + @extension ||= static_file.filename.to_s.split('.').last end # Obtener la ruta al archivo @@ -97,7 +101,7 @@ class MetadataFile < MetadataTemplate end def relative_destination_path - File.join('public', [static_file.key, extension].join('.')) + File.join('public', static_file.key, static_file.filename.to_s) end def destination_path diff --git a/app/models/post.rb b/app/models/post.rb index 14f7ce43..0dd9299f 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -156,8 +156,8 @@ class Post < OpenStruct # Guarda los cambios # rubocop:disable Metrics/CyclomaticComplexity - def save - return false unless valid? + def save(validation = true) + return false if validation && !valid? # Salir si tenemos que cambiar el nombre del archivo y no pudimos return false if !new? && path_changed? && !update_path! return false unless save_attributes! diff --git a/app/models/site.rb b/app/models/site.rb index 72d9cced..d81b95f9 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -39,7 +39,7 @@ class Site < ApplicationRecord # Carga el sitio Jekyll una vez que se inicializa el modelo o después # de crearlo after_initialize :load_jekyll - after_create :load_jekyll + after_create :load_jekyll, :static_file_migration! # Cambiar el nombre del directorio before_update :update_name! # Guardar la configuración si hubo cambios @@ -323,6 +323,11 @@ class Site < ApplicationRecord config.url = url end + # Migra los archivos a Sutty + def static_file_migration! + Site::StaticFileMigration.new(site: self).migrate! + end + # Valida si el sitio tiene al menos una forma de alojamiento asociada # y es la local # diff --git a/app/models/site/static_file_migration.rb b/app/models/site/static_file_migration.rb new file mode 100644 index 00000000..4d08fc8c --- /dev/null +++ b/app/models/site/static_file_migration.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +class Site + MigrationAuthor = Struct.new :email, :name, keyword_init: true + + # Obtiene todos los archivos relacionados en artículos del sitio y los + # sube a Sutty de forma que los podamos seguir utilizando normalmente + # sin casos especiales (ej. soportar archivos locales al repositorio y + # remotos, alojados en Sutty) + # + # TODO: Convertir en reutilizable, por ejemplo correr en cada pull, no + # asumir que la migración se hizo una sola vez... + class StaticFileMigration + # Tipos de metadatos que contienen archivos + STATIC_TYPES = %w[file image].freeze + + attr_reader :site + + def initialize(site:) + @site = site + end + + # Recorre todos los artículos cuyos layouts contengan campos con + # archivos estáticos + def migrate! + log = File.open(File.join(site.path, 'migration.log'), 'w') + modified = [] + + Dir.chdir site.path do + site.locales.each do |locale| + # Recorrer todos los documentos de todas las colecciones + site.posts(lang: locale).each do |doc| + # Ignoramos los documentos cuyo layout no contiene archivos + next unless layouts.include? doc.layout.name + + remove = true + + # Buscamos todos los campos con archivos + fields.each do |field| + next unless doc.attribute? field + next unless doc.document.data.key? field.to_s + + # Traemos los metadatos, en este punto, Sutty cree que el + # archivo está subido, porque es una string apuntando a un + # archivo. + metadata = doc.public_send(field) + + next if metadata.value['path'].blank? + + path = Pathname.new(metadata.value['path']) + + # Si no existe, agregamos una imagen faltante para no + # romper el sitio en Sutty + unless path.exist? + log.write "#{path} no existe\n" + path = Pathname.new(Rails.root.join('app/assets/images/logo.png')) + remove = false + end + + # Agregamos el archivo al sitio y se lo asignamos al campo + metadata.value['path'] = { + io: path.open, + filename: path.basename + } + + # Copiar y analizar el archivo sincrónicamente + metadata.static_file.blob.upload path.open + metadata.static_file.blob.analyze + + next unless remove + + dest = Pathname.new(metadata.send(:relative_destination_path)) + + # Eliminamos el archivo original y lo vinculamos al subido + # para mantener la ruta y no romper el sitio + FileUtils.rm_f path + # XXX: Link simbólico o duro? + FileUtils.ln_s dest.relative_path_from(path.dirname), path + end + + # Guardamos los cambios + unless doc.save(false) + log.write "#{doc.path.relative} no se pudo guardar\n" + end + + modified << doc.path.absolute + end + end + end + + log.close + + # TODO: Hacer la migración desde el servicio de creación de sitios? + site.repository.commit(file: modified, + message: I18n.t('sites.static_file_migration'), + usuarie: author) + end + + private + + def author + @author = MigrationAuthor.new email: "sutty@#{Site.domain}", + name: 'Sutty' + end + + # Encuentra todos los layouts con campos estáticos + def layouts + @layouts ||= site.layouts.reject do |_, layout| + layout.metadata.select do |_, desc| + STATIC_TYPES.include? desc['type'] + end.empty? + end.keys + end + + # Encuentra todos los campos con archivos estáticos + def fields + @fields ||= layouts.map do |layout| + site.layouts[layout].metadata.select do |_, desc| + STATIC_TYPES.include? desc['type'] + end.keys + end.flatten.uniq.map(&:to_sym) + end + end +end diff --git a/config/initializers/analyze_job.rb b/config/initializers/analyze_job.rb new file mode 100644 index 00000000..f268e0dd --- /dev/null +++ b/config/initializers/analyze_job.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# TODO: Estamos procesando el análisis de los archivos en el momento +# porque queremos obtener la ruta del archivo en el momento y no +# después. Necesitaríamos poder generar el vínculo en el +# repositorio a destiempo, modificando el Job de ActiveStorage +ActiveStorage::AnalyzeJob.queue_adapter = :inline diff --git a/config/locales/en.yml b/config/locales/en.yml index 466c4324..ff7278e9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -148,6 +148,7 @@ en: anexo: 'Appendix' simple: 'Simple' sites: + static_file_migration: 'File migration' index: 'This is the list of sites you can edit.' edit_translations: "You can edit texts from your site other than posts', and you can also translate them to other languages." diff --git a/config/locales/es.yml b/config/locales/es.yml index 4c104fcb..7d2b1dfd 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -149,6 +149,7 @@ es: anexo: 'Anexo' simple: 'Simple' sites: + static_file_migration: 'Migración de archivos' index: 'Este es el listado de sitios que puedes editar.' edit_translations: 'Puedes editar los textos que salen en tu sitio que no corresponden a artículos aquí, además de traducirlos a diff --git a/doc/static_files.md b/doc/static_files.md new file mode 100644 index 00000000..8a7c91a3 --- /dev/null +++ b/doc/static_files.md @@ -0,0 +1,30 @@ +# Archivos estáticos + +El objetivo es encontrar una forma de importar los archivos estáticos de +un sitio que se ha migrado a la estructura de Sutty, es decir, subirlos +como archivos de Rails y luego vincularlos a los artículos. + +Actualmente, los archivos primero se suben a la interfaz via +ActiveStorage y luego se vinculan internamente al repositorio[^git]. + +Lo que deberíamos lograr es reconocer los archivos vinculados desde los +artículos y hacer el proceso de cargarlos internamente, modificando los +artículos para que apunten a las nuevas versiones. + +Esto podría hacerse por cada artículo individual o durante el proceso de +creación del sitio, después de clonarlo. + +El algoritmo sería: + +* Clonar el sitio +* Obtener todos los artículos de todas las colecciones +* Encontrar todos los layouts con archivos adjuntos +* Buscar todos los artículos que tengan esos layouts +* Tomar el archivo físico desde los metadatos y asociarlo al sitio via Active Storage +* Asociar el archivo al MetadataFile/Image correspondiente +* ??? +* Migración cumplida! + + +[^git]: esto puede hacer que los repositorios git sean gigantes, + podríamos usar algo como git-annex quizás? diff --git a/test/models/post_test.rb b/test/models/post_test.rb index 7b903b6a..827aeb5c 100644 --- a/test/models/post_test.rb +++ b/test/models/post_test.rb @@ -63,6 +63,12 @@ class PostTest < ActiveSupport::TestCase assert_not @post.valid? end + test 'se pueden guardar sin validar' do + assert @post.valid? + @post.title.value = '' + assert @post.save(false) + end + test 'se pueden guardar los cambios' do title = SecureRandom.hex @post.title.value = title