generar imágenes a demanda
relacionado con #213 closes #309 closes #310 closes #311 closes #312 closes #313
This commit is contained in:
parent
2a395dcc48
commit
c050d861bf
3 changed files with 69 additions and 118 deletions
|
@ -30,7 +30,7 @@ class MetadataFile < MetadataTemplate
|
||||||
|
|
||||||
# Determina si el archivo ya fue subido
|
# Determina si el archivo ya fue subido
|
||||||
def uploaded?
|
def uploaded?
|
||||||
value['path'].is_a?(String) && path?
|
value['path'].is_a?(String)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Determina si la ruta es opcional pero deja pasar si la ruta se
|
# Determina si la ruta es opcional pero deja pasar si la ruta se
|
||||||
|
@ -40,15 +40,17 @@ class MetadataFile < MetadataTemplate
|
||||||
end
|
end
|
||||||
|
|
||||||
# Asociar la imagen subida al sitio y obtener la ruta
|
# 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
|
def save
|
||||||
return true unless changed?
|
|
||||||
|
|
||||||
value['description'] = sanitize value['description']
|
value['description'] = sanitize value['description']
|
||||||
value['path'] = nil unless path?
|
value['path'] = nil unless path?
|
||||||
|
|
||||||
return true if uploaded?
|
|
||||||
return true if path_optional?
|
return true if path_optional?
|
||||||
return false unless hardlink.zero?
|
return false unless hardlink
|
||||||
|
|
||||||
# Modificar el valor actual
|
# Modificar el valor actual
|
||||||
value['path'] = relative_destination_path
|
value['path'] = relative_destination_path
|
||||||
|
@ -56,60 +58,73 @@ class MetadataFile < MetadataTemplate
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
# Almacena el archivo en el sitio y lo devuelve
|
# Almacena el archivo en el sitio y lo devuelve o lo obtiene de la
|
||||||
|
# base de datos.
|
||||||
#
|
#
|
||||||
# @return ActiveStorage::Attachment
|
# 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
|
def static_file
|
||||||
return @static_file if @static_file
|
|
||||||
return unless path?
|
return unless path?
|
||||||
|
|
||||||
ActiveRecord::Base.connection_pool.with_connection do
|
@static_file ||=
|
||||||
if uploaded?
|
case value['path']
|
||||||
blob = ActiveStorage::Blob.find_by(key: key_from_path)
|
when ActionDispatch::Http::UploadedFile
|
||||||
@static_file ||= site.static_files.find_by(blob_id: blob.id)
|
site.static_files.last if site.static_files.attach(value['path'])
|
||||||
elsif site.static_files.attach(value['path'])
|
when String
|
||||||
@static_file ||= site.static_files.last
|
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
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def key_from_path
|
def key_from_path
|
||||||
path.dirname.basename.to_s
|
path.dirname.basename.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def path?
|
def path?
|
||||||
value['path'].present?
|
value['path'].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def filemagic
|
def filemagic
|
||||||
@filemagic ||= FileMagic.new(FileMagic::MAGIC_MIME)
|
@filemagic ||= FileMagic.new(FileMagic::MAGIC_MIME)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [Pathname]
|
||||||
def path
|
def path
|
||||||
@path ||= Pathname.new value['path']
|
@path ||= Pathname.new(File.join(site.path, value['path']))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Obtiene la ruta absoluta del archivo
|
|
||||||
#
|
|
||||||
# @return [String|Nil]
|
|
||||||
def file
|
def file
|
||||||
return unless path?
|
@file ||=
|
||||||
|
case value['path']
|
||||||
if value['path'].is_a? ActionDispatch::Http::UploadedFile
|
when ActionDispatch::Http::UploadedFile then value['path'].tempfile.path
|
||||||
value['path'].tempfile.path
|
when String then File.join(site.path, value['path'])
|
||||||
else
|
end
|
||||||
File.join site.path, value['path']
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Hacemos un link duro para colocar el archivo dentro del repositorio
|
# Hacemos un link duro para colocar el archivo dentro del repositorio
|
||||||
# y no duplicar el espacio que ocupan. Esto requiere que ambos
|
# y no duplicar el espacio que ocupan. Esto requiere que ambos
|
||||||
# directorios estén dentro del mismo punto de montaje.
|
# 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
|
def hardlink
|
||||||
FileUtils.mkdir_p File.dirname(destination_path)
|
FileUtils.mkdir_p(File.dirname(destination_path))
|
||||||
FileUtils.ln uploaded_path, destination_path
|
FileUtils.ln(uploaded_path, destination_path).zero?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Obtener la ruta al archivo
|
# Obtener la ruta al archivo
|
||||||
|
@ -118,16 +133,23 @@ class MetadataFile < MetadataTemplate
|
||||||
ActiveStorage::Blob.service.path_for(static_file.key)
|
ActiveStorage::Blob.service.path_for(static_file.key)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
def uploaded_path
|
def uploaded_path
|
||||||
Rails.root.join uploaded_relative_path
|
Rails.root.join uploaded_relative_path
|
||||||
end
|
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
|
def relative_destination_path
|
||||||
File.join('public', static_file.key, static_file.filename.to_s)
|
@relative_destination_path ||= File.join('public', static_file.key, static_file.filename.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
def destination_path
|
def destination_path
|
||||||
File.join(site.path, relative_destination_path)
|
@destination_path ||= File.join(site.path, relative_destination_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
# No hay archivo pero se lo describió
|
# No hay archivo pero se lo describió
|
||||||
|
|
|
@ -2,15 +2,10 @@
|
||||||
|
|
||||||
class Site
|
class Site
|
||||||
# Obtiene todos los archivos relacionados en artículos del sitio y los
|
# Obtiene todos los archivos relacionados en artículos del sitio y los
|
||||||
# sube a Sutty de forma que los podamos seguir utilizando normalmente
|
# sube a Sutty.
|
||||||
# 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
|
class StaticFileMigration
|
||||||
# Tipos de metadatos que contienen archivos
|
# Tipos de metadatos que contienen archivos
|
||||||
STATIC_TYPES = %w[file image].freeze
|
STATIC_TYPES = %i[file image].freeze
|
||||||
|
|
||||||
attr_reader :site
|
attr_reader :site
|
||||||
|
|
||||||
|
@ -18,72 +13,20 @@ class Site
|
||||||
@site = site
|
@site = site
|
||||||
end
|
end
|
||||||
|
|
||||||
# Recorre todos los artículos cuyos layouts contengan campos con
|
|
||||||
# archivos estáticos
|
|
||||||
def migrate!
|
def migrate!
|
||||||
log = File.open(File.join(site.path, 'migration.log'), 'w')
|
modified = site.docs.map do |doc|
|
||||||
modified = []
|
next unless STATIC_TYPES.map do |field|
|
||||||
|
next unless doc.attribute? field
|
||||||
|
next unless doc[field].path?
|
||||||
|
next unless doc[field].static_file
|
||||||
|
|
||||||
Dir.chdir site.path do
|
true
|
||||||
site.locales.each do |locale|
|
end.any?
|
||||||
# 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
|
|
||||||
|
|
||||||
# Buscamos todos los campos con archivos
|
log.write "#{doc.path.relative};no se pudo guardar\n" unless doc.save(validate: false)
|
||||||
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
|
doc.path.absolute
|
||||||
# archivo está subido, porque es una string apuntando a un
|
end.compact
|
||||||
# archivo.
|
|
||||||
metadata = doc.public_send(field)
|
|
||||||
|
|
||||||
next if metadata.value['path'].blank?
|
|
||||||
next if ActiveStorage::Blob.find_by(key: metadata.key_from_path)
|
|
||||||
|
|
||||||
path = Pathname.new(metadata.value['path'])
|
|
||||||
|
|
||||||
# Si no existe vaciamos el campo
|
|
||||||
unless path.exist?
|
|
||||||
metadata.value['path'] = nil
|
|
||||||
log.write "el archivo #{path} de #{doc.path.relative} no existe"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
# Agregamos el archivo al sitio y se lo asignamos al campo
|
|
||||||
# XXX: No usamos ActionDispatch::Http::UploadedFile porque
|
|
||||||
# no tenemos forma de crear un Tempfile o equivalente a
|
|
||||||
# partir de un archivo que exista.
|
|
||||||
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
|
|
||||||
|
|
||||||
dest = Pathname.new(metadata.send(:relative_destination_path))
|
|
||||||
metadata.value['path'] = dest.to_s
|
|
||||||
metadata.send(:hardlink)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
log.write "#{doc.path.relative} no se pudo guardar\n" unless doc.save(validate: false)
|
|
||||||
|
|
||||||
modified << doc.path.absolute
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
log.close
|
log.close
|
||||||
|
|
||||||
|
@ -102,22 +45,8 @@ class Site
|
||||||
name: 'Sutty'
|
name: 'Sutty'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Encuentra todos los layouts con campos estáticos
|
def log
|
||||||
def layouts
|
@log ||= File.open(File.join(site.path, 'migration.csv'), 'w')
|
||||||
@layouts ||= site.layouts.to_h.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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,7 +32,7 @@ class RepositoryTest < ActiveSupport::TestCase
|
||||||
.branches['master'].target.author[:name]
|
.branches['master'].target.author[:name]
|
||||||
|
|
||||||
Dir.chdir(@site.path) do
|
Dir.chdir(@site.path) do
|
||||||
FileUtils.rm 'migration.log'
|
FileUtils.rm 'migration.csv'
|
||||||
|
|
||||||
assert_equal 'nothing to commit, working tree clean',
|
assert_equal 'nothing to commit, working tree clean',
|
||||||
`LC_ALL=C git status`.strip.split("\n").last
|
`LC_ALL=C git status`.strip.split("\n").last
|
||||||
|
|
Loading…
Reference in a new issue