WIP: Subida de archivos
This commit is contained in:
parent
80457588dd
commit
0a1cbb9fc2
18 changed files with 265 additions and 43 deletions
4
Gemfile
4
Gemfile
|
@ -35,9 +35,6 @@ gem 'bcrypt', '~> 3.1.7'
|
|||
# gem 'capistrano-rails', group: :development
|
||||
|
||||
gem 'bootstrap', '~> 4'
|
||||
gem 'carrierwave'
|
||||
gem 'carrierwave-bombshelter'
|
||||
gem 'carrierwave-i18n'
|
||||
gem 'commonmarker'
|
||||
gem 'devise'
|
||||
gem 'devise-i18n'
|
||||
|
@ -48,6 +45,7 @@ gem 'font-awesome-rails'
|
|||
gem 'friendly_id'
|
||||
gem 'hamlit-rails'
|
||||
gem 'hiredis'
|
||||
gem 'image_processing'
|
||||
gem 'jekyll'
|
||||
gem 'jquery-rails'
|
||||
gem 'mini_magick'
|
||||
|
|
22
Gemfile.lock
22
Gemfile.lock
|
@ -80,15 +80,6 @@ GEM
|
|||
rack (>= 1.0.0)
|
||||
rack-test (>= 0.5.4)
|
||||
xpath (>= 2.0, < 4.0)
|
||||
carrierwave (1.3.1)
|
||||
activemodel (>= 4.0.0)
|
||||
activesupport (>= 4.0.0)
|
||||
mime-types (>= 1.16)
|
||||
carrierwave-bombshelter (0.2.2)
|
||||
activesupport (>= 3.2.0)
|
||||
carrierwave
|
||||
fastimage
|
||||
carrierwave-i18n (0.2.0)
|
||||
childprocess (0.9.0)
|
||||
ffi (~> 1.0, >= 1.0.11)
|
||||
coderay (1.1.2)
|
||||
|
@ -132,7 +123,6 @@ GEM
|
|||
factory_bot_rails (5.0.2)
|
||||
factory_bot (~> 5.0.2)
|
||||
railties (>= 4.2.0)
|
||||
fastimage (2.1.5)
|
||||
ffi (1.11.1)
|
||||
font-awesome-rails (4.7.0.4)
|
||||
railties (>= 3.2, < 6.0)
|
||||
|
@ -165,6 +155,9 @@ GEM
|
|||
http_parser.rb (0.6.0)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.9.1)
|
||||
mini_magick (>= 4.9.3, < 5)
|
||||
ruby-vips (>= 2.0.13, < 3)
|
||||
jaro_winkler (1.5.3)
|
||||
jbuilder (2.8.0)
|
||||
activesupport (>= 4.2.0)
|
||||
|
@ -209,9 +202,6 @@ GEM
|
|||
mimemagic (~> 0.3.2)
|
||||
mercenary (0.3.6)
|
||||
method_source (0.9.2)
|
||||
mime-types (3.2.2)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2018.0812)
|
||||
mimemagic (0.3.3)
|
||||
mini_magick (4.9.4)
|
||||
mini_mime (1.0.1)
|
||||
|
@ -319,6 +309,8 @@ GEM
|
|||
ruby-enum (0.7.2)
|
||||
i18n
|
||||
ruby-progressbar (1.10.1)
|
||||
ruby-vips (2.0.14)
|
||||
ffi (~> 1.9)
|
||||
ruby_dep (1.5.0)
|
||||
rubyzip (1.2.2)
|
||||
rugged (0.28.2)
|
||||
|
@ -416,9 +408,6 @@ DEPENDENCIES
|
|||
capistrano-rails
|
||||
capistrano-rbenv
|
||||
capybara (~> 2.13)
|
||||
carrierwave
|
||||
carrierwave-bombshelter
|
||||
carrierwave-i18n
|
||||
commonmarker
|
||||
database_cleaner
|
||||
devise
|
||||
|
@ -434,6 +423,7 @@ DEPENDENCIES
|
|||
haml-lint
|
||||
hamlit-rails
|
||||
hiredis
|
||||
image_processing
|
||||
jbuilder (~> 2.5)
|
||||
jekyll
|
||||
jquery-rails
|
||||
|
|
11
app/assets/javascripts/image_preview.js
Normal file
11
app/assets/javascripts/image_preview.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
$(document).on('turbolinks:load', function() {
|
||||
$('input[type=file]').on('change', function(event) {
|
||||
if (event.target.files.length == 0) return;
|
||||
|
||||
var input = $(event.target);
|
||||
var preview = $(`#${input.data('preview')}`);
|
||||
|
||||
preview.attr('src',
|
||||
window.URL.createObjectURL(event.target.files[0]));
|
||||
});
|
||||
});
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Define un campo de imagen
|
||||
# TODO: Validar que sea una imagen
|
||||
class MetadataImage < MetadataTemplate
|
||||
# Una ruta vacía a la imagen con una descripción vacía
|
||||
def default_value
|
||||
|
@ -11,7 +12,73 @@ class MetadataImage < MetadataTemplate
|
|||
value == default_value
|
||||
end
|
||||
|
||||
# Asociar la imagen subida al sitio y obtener la ruta
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def save
|
||||
return true if !required && value['path'].blank?
|
||||
return false if required && value['path'].blank?
|
||||
return true if value['path'].is_a? String
|
||||
return false unless hardlink.zero?
|
||||
|
||||
# Modificar el valor actual
|
||||
value['path'] = relative_destination_path
|
||||
|
||||
true
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
def to_param
|
||||
{ name => %i[description path] }
|
||||
end
|
||||
|
||||
# Almacena el archivo en el sitio y lo devuelve
|
||||
# XXX: ActiveStorage devuelve un Array al guardar
|
||||
#
|
||||
# @return ActiveStorage::Attachment
|
||||
def static_file
|
||||
if value['path'].is_a? String
|
||||
blob = ActiveStorage::Blob.find_by(key: key_from_path)
|
||||
@static_file ||= site.static_files.find_by(blob_id: blob.id)
|
||||
else
|
||||
@static_file ||= site.static_files.attach(value['path']).first
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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'], '.*')
|
||||
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.
|
||||
def hardlink
|
||||
FileUtils.mkdir_p File.dirname(destination_path)
|
||||
FileUtils.ln uploaded_path, destination_path
|
||||
end
|
||||
|
||||
def extension
|
||||
static_file.blob.content_type.split('/').last
|
||||
end
|
||||
|
||||
# Obtener la ruta al archivo
|
||||
# https://stackoverflow.com/a/53908358
|
||||
def uploaded_relative_path
|
||||
ActiveStorage::Blob.service.path_for(static_file.key)
|
||||
end
|
||||
|
||||
def uploaded_path
|
||||
Rails.root.join uploaded_relative_path
|
||||
end
|
||||
|
||||
def relative_destination_path
|
||||
File.join('public', [static_file.key, extension].join('.'))
|
||||
end
|
||||
|
||||
def destination_path
|
||||
File.join(site.path, relative_destination_path)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -55,6 +55,12 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
|||
type == 'array'
|
||||
end
|
||||
|
||||
# En caso de que algún campo necesite realizar acciones antes de ser
|
||||
# guardado
|
||||
def save
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Si es obligatorio no puede estar vacío
|
||||
|
|
|
@ -149,10 +149,12 @@ class Post < OpenStruct
|
|||
alias destroy! destroy
|
||||
|
||||
# Guarda los cambios
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def save
|
||||
return false unless 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!
|
||||
return false unless write
|
||||
|
||||
# Vuelve a leer el post para tomar los cambios
|
||||
|
@ -160,6 +162,7 @@ class Post < OpenStruct
|
|||
|
||||
true
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
alias save! save
|
||||
|
||||
# Lee el documento a menos que estemos trabajando con un documento en
|
||||
|
@ -282,6 +285,13 @@ class Post < OpenStruct
|
|||
type: :path, post: self,
|
||||
required: true)
|
||||
end
|
||||
|
||||
# Ejecuta la acción de guardado en cada atributo
|
||||
def save_attributes!
|
||||
attributes.map do |attr|
|
||||
send(attr).save
|
||||
end.all?
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/ClassLength
|
||||
# rubocop:enable Style/MethodMissingSuper
|
||||
|
|
|
@ -25,6 +25,9 @@ class Site < ApplicationRecord
|
|||
has_many :invitades, -> { where('roles.rol = ?', 'invitade') },
|
||||
through: :roles, source: :usuarie
|
||||
|
||||
# Mantenemos el nombre que les da Jekyll
|
||||
has_many_attached :static_files
|
||||
|
||||
# Clonar el directorio de esqueleto antes de crear el sitio
|
||||
before_create :clone_skel!
|
||||
# Elimina el directorio al destruir un sitio
|
||||
|
|
|
@ -3,5 +3,7 @@
|
|||
%td
|
||||
- if metadata.value['path'].present?
|
||||
%figure
|
||||
= image_tag metadata.value['path'], alt: metadata.value['description']
|
||||
= image_tag url_for(metadata.static_file),
|
||||
alt: metadata.value['description'],
|
||||
class: 'img-fluid'
|
||||
%figcaption= metadata.value['description']
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
.form-group{ class: invalid(post, attribute) }
|
||||
- if metadata.value['path'].present?
|
||||
= image_tag metadata.value[:path], alt: metadata.value['description']
|
||||
= image_tag url_for(metadata.static_file),
|
||||
alt: metadata.value['description'],
|
||||
class: 'img-fluid',
|
||||
id: "#{attribute}-preview"
|
||||
|
||||
-#
|
||||
Mantener el valor si no enviamos ninguna imagen
|
||||
TODO: Agregar checkbox para eliminarla
|
||||
= hidden_field_tag "post[#{attribute}][path]", metadata.value['path']
|
||||
|
||||
.custom-file
|
||||
= file_field(*field_name_for('post', attribute, :path),
|
||||
**field_options(attribute, metadata), class: 'custom-file-input')
|
||||
**field_options(attribute, metadata),
|
||||
class: 'custom-file-input', accept: 'image/*',
|
||||
data: { preview: "#{attribute}-preview" })
|
||||
= label_tag "post_#{attribute}_path",
|
||||
post_label_t(attribute, :path, post: post), class: 'custom-file-label'
|
||||
= render 'posts/attribute_feedback',
|
||||
|
|
|
@ -13,6 +13,7 @@ require 'action_view/railtie'
|
|||
# require "action_cable/engine"
|
||||
require 'sprockets/railtie'
|
||||
require 'rails/test_unit/railtie'
|
||||
require 'active_storage/engine'
|
||||
|
||||
# Require the gems listed in Gemfile, including any gems
|
||||
# you've limited to :test, :development, or :production.
|
||||
|
@ -32,5 +33,7 @@ module Sutty
|
|||
config.action_dispatch
|
||||
.rescue_responses['Pundit::NotAuthorizedError'] = :forbidden
|
||||
config.active_record.sqlite3.represent_boolean_as_integer = true
|
||||
|
||||
config.active_storage.variant_processor = :vips
|
||||
end
|
||||
end
|
||||
|
|
|
@ -57,15 +57,11 @@ Rails.application.configure do
|
|||
# listen gem.
|
||||
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
|
||||
|
||||
CarrierWave.configure do |config|
|
||||
config.ignore_integrity_errors = false
|
||||
config.ignore_processing_errors = false
|
||||
config.ignore_download_errors = false
|
||||
end
|
||||
|
||||
config.action_mailer.perform_caching = false
|
||||
config.action_mailer.delivery_method = :letter_opener
|
||||
config.action_mailer.perform_deliveries = true
|
||||
config.action_mailer.default_url_options = { host: 'localhost',
|
||||
port: 3000 }
|
||||
|
||||
config.active_storage.service = :local
|
||||
end
|
||||
|
|
|
@ -107,4 +107,5 @@ Rails.application.configure do
|
|||
sender_address: ENV['DEFAULT_FROM'],
|
||||
exception_recipients: ENV['EXCEPTION_TO']
|
||||
}
|
||||
config.active_storage.service = :local
|
||||
end
|
||||
|
|
|
@ -45,4 +45,5 @@ Rails.application.configure do
|
|||
|
||||
# Raises error for missing translations
|
||||
# config.action_view.raise_on_missing_translations = true
|
||||
config.active_storage.service = :test
|
||||
end
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
CarrierWave.configure do |config|
|
||||
config.permissions = 0o640
|
||||
config.directory_permissions = 0o750
|
||||
config.storage = :file
|
||||
end
|
15
config/storage.yml
Normal file
15
config/storage.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
# XXX: Dirty hack
|
||||
#
|
||||
# Almacenamos los archivos dentro del directorio de sitios para poder
|
||||
# hacer links duros. Si montamos en Docker los dos directorios por
|
||||
# separado, aunque en el servidor estén los directorios dentro del mismo
|
||||
# sistema de archivos, para Docker son dos distintos.
|
||||
#
|
||||
# Ver app/models/metadata_image.rb
|
||||
local:
|
||||
service: Disk
|
||||
root: <%= Rails.root.join('_sites/_storage') %>
|
||||
|
||||
test:
|
||||
service: Disk
|
||||
root: <%= Rails.root.join('tmp/storage') %>
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This migration comes from active_storage (originally 20170806125915)
|
||||
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :active_storage_blobs do |t|
|
||||
t.string :key, null: false
|
||||
t.string :filename, null: false
|
||||
t.string :content_type
|
||||
t.text :metadata
|
||||
t.bigint :byte_size, null: false
|
||||
t.string :checksum, null: false
|
||||
t.datetime :created_at, null: false
|
||||
|
||||
t.index [:key], unique: true
|
||||
end
|
||||
|
||||
create_table :active_storage_attachments do |t|
|
||||
t.string :name, null: false
|
||||
t.references :record, null: false, polymorphic: true, index: false
|
||||
t.references :blob, null: false
|
||||
|
||||
t.datetime :created_at, null: false
|
||||
|
||||
t.index %i[record_type record_id name blob_id], name: 'index_active_storage_attachments_uniqueness', unique: true
|
||||
t.foreign_key :active_storage_blobs, column: :blob_id
|
||||
end
|
||||
end
|
||||
end
|
23
db/schema.rb
23
db/schema.rb
|
@ -12,7 +12,28 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20_190_730_211_756) do
|
||||
ActiveRecord::Schema.define(version: 20_190_820_225_238) do
|
||||
create_table 'active_storage_attachments', force: :cascade do |t|
|
||||
t.string 'name', null: false
|
||||
t.string 'record_type', null: false
|
||||
t.integer 'record_id', null: false
|
||||
t.integer 'blob_id', null: false
|
||||
t.datetime 'created_at', null: false
|
||||
t.index ['blob_id'], name: 'index_active_storage_attachments_on_blob_id'
|
||||
t.index %w[record_type record_id name blob_id], name: 'index_active_storage_attachments_uniqueness', unique: true
|
||||
end
|
||||
|
||||
create_table 'active_storage_blobs', force: :cascade do |t|
|
||||
t.string 'key', null: false
|
||||
t.string 'filename', null: false
|
||||
t.string 'content_type'
|
||||
t.text 'metadata'
|
||||
t.bigint 'byte_size', null: false
|
||||
t.string 'checksum', null: false
|
||||
t.datetime 'created_at', null: false
|
||||
t.index ['key'], name: 'index_active_storage_blobs_on_key', unique: true
|
||||
end
|
||||
|
||||
create_table 'build_stats', force: :cascade do |t|
|
||||
t.datetime 'created_at', null: false
|
||||
t.datetime 'updated_at', null: false
|
||||
|
|
|
@ -5,17 +5,83 @@ plantilla:
|
|||
|
||||
```yaml
|
||||
---
|
||||
cover: image
|
||||
attachment: document
|
||||
video: video
|
||||
audio: audio
|
||||
cover:
|
||||
type: image
|
||||
attachment:
|
||||
type: document
|
||||
video:
|
||||
type: video
|
||||
audio:
|
||||
type: audio
|
||||
---
|
||||
```
|
||||
|
||||
Donde `image` solo admite imágenes, `document` cualquier tipo de documento y
|
||||
`video` y `audio` medios.
|
||||
Donde `image` solo admite imágenes, `document` cualquier tipo de
|
||||
documento y `video` y `audio` medios.
|
||||
|
||||
Al subir los archivos, se guardan en el directorio `public/` en la raíz
|
||||
del proyecto Jekyll.
|
||||
del sitio.
|
||||
|
||||
En el frontmatter solo sale la URL del archivo asociado.
|
||||
|
||||
## ActiveStorage
|
||||
|
||||
ActiveStorage es el método por defecto para subida de archivos en Ruby
|
||||
on Rails. Sin embargo, no nos permite decidir dónde queremos guardar
|
||||
los archivos, sino que los guarda directamente relacionados a un modelo
|
||||
de la base de datos, en una ruta generada automáticamente.
|
||||
|
||||
Podríamos implementar un proveedor de ActiveStorage, pero la
|
||||
documentación para hacerlo es escasa.
|
||||
|
||||
Como tenemos que asociarlo a un modelo, lo correcto sería usar el modelo
|
||||
Site, de forma que tengamos acceso a todas las imágenes. Esto abriría
|
||||
la posibilidad de tener una galería de imágenes y poder seleccionar
|
||||
imágenes entre las ya subidas o subir nuevas.
|
||||
|
||||
Pero como los archivos físicos se guardan directamente en una ruta
|
||||
indicada por ActiveStorage, empezamos a utilizar más espacio porque para
|
||||
que los sitios sean autocontenidos, deberíamos copiarlos al directorio
|
||||
del sitio. También podemos utilizar enlaces duros (_hardlinks_) para no
|
||||
ocupar espacio extra.
|
||||
|
||||
Ya que las imágenes van a estar dentro del directorio de Sutty en lugar
|
||||
de cada archivo, es más fácil poder visualizarlas, sin tener que hacer
|
||||
hacks enviando el archivo a través de Rails. Con AS podemos vincularlas
|
||||
directamente desde el sistema de archivos.
|
||||
|
||||
## CarrierWave
|
||||
|
||||
CarrierWave es la forma en que estamos subiendo los archivos hasta el
|
||||
momento. Puede transformar las imágenes a distintas resoluciones
|
||||
automáticamente (aunque quisiéramos hacer esto durante la generación del
|
||||
sitio). También hemos tenido problemas con los nombres de los archivos
|
||||
y encontrado errores no documentados con respecto a cuándo cambia el
|
||||
nombre del archivo o no, con lo que usar CW se nos hizo un poco endeble.
|
||||
|
||||
## Subida
|
||||
|
||||
Entonces:
|
||||
|
||||
* Activamos ActiveStorage local (ofrece nubes, pero no queremos ninguna
|
||||
nube)
|
||||
|
||||
* Asociamos archivos al modelo Site
|
||||
|
||||
* Al subir un archivo desde el editor de Post, se asocia al Site, no al
|
||||
Post.
|
||||
|
||||
* Para asociar el archivo al Post, generamos un hardlink con la misma
|
||||
ruta, dentro del directorio del sitio. Esto nos permite vincular
|
||||
ambos archivos sin agregar metadatos adicionales.
|
||||
|
||||
* Para generar variantes del archivo, lo hacemos con un plugin Jekyll
|
||||
que las genera dentro del sitio si no existen y que agrega los
|
||||
atributos srcset para imágenes responsive.
|
||||
|
||||
* Si encontramos forma, aplicamos `oxipng` y otros optimizadores. Sino,
|
||||
lo haremos desde el plugin.
|
||||
|
||||
## Dependencias
|
||||
|
||||
Usamos VIPS para procesar imágenes con bajo consumo de recursos
|
||||
|
|
Loading…
Reference in a new issue