5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-16 12:31:42 +00:00

Merge branch 'rails' into recuperar-partials

This commit is contained in:
f 2022-07-19 13:48:31 -03:00
commit 7272a37e23
31 changed files with 420 additions and 314 deletions

View file

@ -1,10 +1,4 @@
# Excluir todo # Excluir todo
* *
# Solo agregar lo que usamos en COPY # Solo agregar lo que usamos en COPY
!./.git/ # !./archivo
!./rubygems-platform-musl.patch
!./Gemfile
!./Gemfile.lock
!./config/credentials.yml.enc
!./public/assets/
!./public/packs/

View file

@ -1,125 +1,21 @@
# Este Dockerfile está armado pensando en una compilación lanzada desde FROM registry.nulo.in/sutty/rails:3.13.6-2.7.5
# el mismo repositorio de trabajo. Cuando tengamos CI/CD algunas cosas ARG PANDOC_VERSION=2.17.1.1
# como el tarball van a tener que cambiar porque ya vamos a haber hecho
# un clone/pull limpio.
FROM alpine:3.13.6 AS build
MAINTAINER "f <f@sutty.nl>"
ARG RAILS_MASTER_KEY
ARG BRANCH
# Un entorno base
ENV BRANCH=$BRANCH
ENV SECRET_KEY_BASE solo_es_necesaria_para_correr_rake
ENV RAILS_ENV production ENV RAILS_ENV production
ENV RAILS_MASTER_KEY=$RAILS_MASTER_KEY
RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-json ruby-bigdecimal ruby-rake
RUN apk add --no-cache postgresql-libs git yarn brotli libssh2 python3
RUN test "2.7.4" = `ruby -e 'puts RUBY_VERSION'`
# https://github.com/rubygems/rubygems/issues/2918
# https://gitlab.alpinelinux.org/alpine/aports/issues/10808
RUN apk add --no-cache patch
COPY ./rubygems-platform-musl.patch /tmp/
RUN cd /usr/lib/ruby/2.7.0 && patch -Np 0 -i /tmp/rubygems-platform-musl.patch
# Agregar el usuario
RUN addgroup -g 82 -S www-data
RUN adduser -s /bin/sh -G www-data -h /home/app -D app
RUN install -dm750 -o app -g www-data /home/app/sutty
RUN gem install --no-document bundler:2.1.4
# Empezamos con la usuaria app
USER app
# Vamos a trabajar dentro de este directorio
WORKDIR /home/app/sutty
# Copiamos solo el Gemfile para poder instalar las gemas necesarias
COPY --chown=app:www-data ./Gemfile .
COPY --chown=app:www-data ./Gemfile.lock .
RUN bundle config set no-cache true
RUN bundle config set specific_platform true
RUN bundle install --path=./vendor --without='test development'
# Vaciar la caché
RUN rm vendor/ruby/2.7.0/cache/*.gem
# Copiar el repositorio git
COPY --chown=app:www-data ./.git/ ./.git/
# Hacer un clon limpio del repositorio en lugar de copiar todos los
# archivos
RUN cd .. && git clone sutty checkout
RUN cd ../checkout && git checkout $BRANCH
WORKDIR /home/app/checkout
# Traer las gemas:
RUN rm -rf ./vendor
RUN mv ../sutty/vendor ./vendor
RUN mv ../sutty/.bundle ./.bundle
# Instalar secretos
COPY --chown=app:root ./config/credentials.yml.enc ./config/
RUN rm -rf ./node_modules ./tmp/cache ./.git ./test ./doc
# Eliminar archivos innecesarios
USER root
RUN apk add --no-cache findutils
RUN find /home/app/checkout/vendor/ruby/2.7.0 -maxdepth 3 -type d -name test -o -name spec -o -name rubocop | xargs -r rm -rf
# Contenedor final
FROM registry.nulo.in/sutty/monit:3.13.6
ENV RAILS_ENV production
# Pandoc
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories
# Instalar las dependencias, separamos la librería de base de datos para # Instalar las dependencias, separamos la librería de base de datos para
# poder reutilizar este primer paso desde otros contenedores # poder reutilizar este primer paso desde otros contenedores
RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-json ruby-bigdecimal ruby-rake ruby-irb ruby-io-console ruby-etc #
RUN apk add --no-cache postgresql-libs libssh2 file rsync git jpegoptim vips
RUN apk add --no-cache ffmpeg imagemagick pandoc tectonic oxipng jemalloc
RUN apk add --no-cache git-lfs openssh-client patch
# Chequear que la versión de ruby sea la correcta
RUN test "2.7.4" = `ruby -e 'puts RUBY_VERSION'`
# https://github.com/rubygems/rubygems/issues/2918
# https://gitlab.alpinelinux.org/alpine/aports/issues/10808
COPY ./rubygems-platform-musl.patch /tmp/
RUN apk add --no-cache patch && cd /usr/lib/ruby/2.7.0 && patch -Np 0 -i /tmp/rubygems-platform-musl.patch && apk del patch
# Necesitamos yarn para que Jekyll pueda generar los sitios # Necesitamos yarn para que Jekyll pueda generar los sitios
# XXX: Eliminarlo cuando extraigamos la generación de sitios del proceso # XXX: Eliminarlo cuando extraigamos la generación de sitios del proceso
# principal # principal
RUN apk add --no-cache yarn RUN apk add --no-cache libxslt libxml2 postgresql-libs libssh2 \
# Instalar foreman para poder correr los servicios rsync git jpegoptim vips tectonic oxipng git-lfs openssh-client \
RUN gem install --no-document --no-user-install bundler:2.1.4 foreman yarn daemonize ruby-webrick
# Agregar el grupo del servidor web y la usuaria RUN gem install --no-document --no-user-install foreman
RUN addgroup -g 82 -S www-data RUN wget https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz -O - | tar --strip-components 1 -xvzf - pandoc-${PANDOC_VERSION}/bin/pandoc && mv /bin/pandoc /usr/bin/pandoc
RUN adduser -s /bin/sh -G www-data -h /srv/http -D app
# Convertirse en app para instalar VOLUME "/srv"
USER app
COPY --from=build --chown=app:www-data /home/app/checkout /srv/http
COPY --chown=app:www-data ./.git/ ./.git/
RUN rm -rf /srv/http/_sites /srv/http/_deploy
RUN ln -s data/_storage /srv/http/_storage
RUN ln -s data/_sites /srv/http/_sites
RUN ln -s data/_deploy /srv/http/_deploy
RUN ln -s data/_private /srv/http/_private
# Volver a root para cerrar la compilación
USER root
# Instalar la configuración de monit
RUN install -m 640 -o root -g root /srv/http/monit.conf /etc/monit.d/sutty.conf
RUN apk add --no-cache daemonize ruby-webrick
RUN install -m 755 /srv/http/entrypoint.sh /usr/local/bin/sutty
# Mantener estos directorios!
VOLUME "/srv/http/data"
# El puerto de puma
EXPOSE 3000 EXPOSE 3000
EXPOSE 9394 EXPOSE 9394

View file

@ -26,7 +26,7 @@ hain ?= ENV_FILE=.env $(HAINISH)## Ubicación de Hainish
# #
# Production es el entorno de panel.sutty.nl # Production es el entorno de panel.sutty.nl
ifeq ($(env),production) ifeq ($(env),production)
container ?= sutty container ?= panel
## TODO: Cambiar a otra cosa ## TODO: Cambiar a otra cosa
branch ?= rails branch ?= rails
public ?= public public ?= public
@ -115,15 +115,9 @@ ota-js: assets ## Actualizar Javascript en el nodo delegado
ssh root@$(delegate) docker exec $(container) sh -c "cat /srv/http/tmp/puma.pid | xargs -r kill -USR2" ssh root@$(delegate) docker exec $(container) sh -c "cat /srv/http/tmp/puma.pid | xargs -r kill -USR2"
ota: ## Actualizar Rails en el nodo delegado ota: ## Actualizar Rails en el nodo delegado
umask 022; git format-patch $(commit) ssh $(delegate) git -C /srv/sutty/srv/http/panel.sutty.nl pull ; true
ssh $(delegate) mkdir -p /tmp/patches-$(commit)/ ssh $(delegate) chown -R 1000:82 /srv/sutty/srv/http/panel.sutty.nl
scp ./0*.patch $(delegate):/tmp/patches-$(commit)/ ssh $(delegate) docker exec $(container) rails reload
scp ./ota.sh $(delegate):/tmp/
ssh $(delegate) docker cp /tmp/patches-$(shell echo $(commit) | cut -d / -f 1) $(container):/tmp/
ssh $(delegate) docker cp /tmp/ota.sh $(container):/usr/local/bin/ota
ssh $(delegate) docker exec $(container) apk add --no-cache patch
ssh $(delegate) docker exec $(container) ota $(commit)
rm ./0*.patch
# Todos los archivos de assets. Si alguno cambia, se van a recompilar # Todos los archivos de assets. Si alguno cambia, se van a recompilar
# los assets que luego se suben al nodo delegado. # los assets que luego se suben al nodo delegado.

View file

@ -67,7 +67,11 @@
.editor-content { .editor-content {
min-height: 480px; min-height: 480px;
p, h1, h2, h3, h4, h5, h6, ul, li, figcaption { outline: #ccc solid thin; } p, h1, h2, h3, h4, h5, h6, ul, li, blockquote, figcaption { outline: #ccc solid thin; }
blockquote {
border-left: #555 solid .25em;
padding: .75em;
}
strong, em, del, u, sub, sup, small { background: #0002; } strong, em, del, u, sub, sup, small { background: #0002; }
a { background: #13fefe50; } a { background: #13fefe50; }
[data-editor-selected] { outline: #f206f9 solid thick; } [data-editor-selected] { outline: #f206f9 solid thick; }

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
module ActiveStorage
# Modifica la creación de un blob antes de subir el archivo para que
# incluya el JekyllService adecuado.
module DirectUploadsControllerDecorator
extend ActiveSupport::Concern
included do
def create
blob = ActiveStorage::Blob.create_before_direct_upload!(service_name: session[:service_name], **blob_args)
render json: direct_upload_json(blob)
end
private
# Normalizar los caracteres unicode en los nombres de archivos
# para que puedan propagarse correctamente a través de todo el
# stack.
def blob_args
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys.tap do |ba|
ba[:filename] = ba[:filename].unicode_normalize
end
end
end
end
end
ActiveStorage::DirectUploadsController.include ActiveStorage::DirectUploadsControllerDecorator

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
module ActiveStorage
# Modificar {DiskController} para poder asociar el blob a un sitio
module DiskControllerDecorator
extend ActiveSupport::Concern
included do
# Asociar el archivo subido al sitio correspondiente. Cada sitio
# tiene su propio servicio de subida de archivos.
def update
if (token = decode_verified_token)
if acceptable_content?(token)
named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
blob = ActiveStorage::Blob.find_by_key token[:key]
site = Site.find_by_name token[:service_name]
site.static_files.attach(blob)
else
head :unprocessable_entity
end
else
head :not_found
end
rescue ActiveStorage::IntegrityError
head :unprocessable_entity
end
end
end
end
ActiveStorage::DiskController.include ActiveStorage::DiskControllerDecorator

View file

@ -6,19 +6,22 @@ module Api
class CspReportsController < BaseController class CspReportsController < BaseController
skip_forgery_protection skip_forgery_protection
# No queremos indicar que algo salió mal
rescue_from ActionController::ParameterMissing, with: :csp_report_created
# Crea un reporte de CSP intercambiando los guiones medios por # Crea un reporte de CSP intercambiando los guiones medios por
# bajos # bajos
# #
# TODO: Aplicar rate_limit # TODO: Aplicar rate_limit
def create def create
csp = CspReport.new(csp_report_params.to_h.map do |k, v| csp = CspReport.new(csp_report_params.to_h.transform_keys do |k|
[k.tr('-', '_'), v] k.tr('-', '_')
end.to_h) end)
csp.id = SecureRandom.uuid csp.id = SecureRandom.uuid
csp.save csp.save
render json: {}, status: :created csp_report_created
end end
private private
@ -39,6 +42,10 @@ module Api
:'column-number', :'column-number',
:'source-file') :'source-file')
end end
def csp_report_created
render json: {}, status: :created
end
end end
end end
end end

View file

@ -6,6 +6,7 @@ class PostsController < ApplicationController
rescue_from Pundit::NilPolicyError, with: :page_not_found rescue_from Pundit::NilPolicyError, with: :page_not_found
before_action :authenticate_usuarie! before_action :authenticate_usuarie!
before_action :service_for_direct_upload, only: %i[new edit]
# TODO: Traer los comunes desde ApplicationController # TODO: Traer los comunes desde ApplicationController
breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path
@ -166,4 +167,9 @@ class PostsController < ApplicationController
def post def post
@post ||= site.posts(lang: locale).find(params[:post_id] || params[:id]) @post ||= site.posts(lang: locale).find(params[:post_id] || params[:id])
end end
# Recuerda el nombre del servicio de subida de archivos
def service_for_direct_upload
session[:service_name] = site.name.to_sym
end
end end

View file

@ -21,11 +21,12 @@ function makeBlock(tag: string): EditorBlock {
} }
export const li: EditorBlock = makeBlock("li"); export const li: EditorBlock = makeBlock("li");
const paragraph: EditorBlock = makeBlock("p");
// XXX: si agregás algo acá, agregalo a blockNames // XXX: si agregás algo acá, agregalo a blockNames
// (y probablemente le quieras hacer un botón en app/views/posts/attributes/_content.haml) // (y probablemente le quieras hacer un botón en app/views/posts/attributes/_content.haml)
export const blocks: { [propName: string]: EditorBlock } = { export const blocks: { [propName: string]: EditorBlock } = {
paragraph: makeBlock("p"), paragraph,
h1: makeBlock("h1"), h1: makeBlock("h1"),
h2: makeBlock("h2"), h2: makeBlock("h2"),
h3: makeBlock("h3"), h3: makeBlock("h3"),
@ -42,6 +43,11 @@ export const blocks: { [propName: string]: EditorBlock } = {
allowedChildren: ["li"], allowedChildren: ["li"],
handleEmpty: li, handleEmpty: li,
}, },
blockquote: {
...makeBlock("blockquote"),
allowedChildren: blockNames,
handleEmpty: paragraph,
},
}; };
export function setupButtons(editor: Editor): void { export function setupButtons(editor: Editor): void {

View file

@ -10,6 +10,7 @@ export const blockNames = [
"h6", "h6",
"unordered_list", "unordered_list",
"ordered_list", "ordered_list",
"blockquote",
]; ];
export const markNames = [ export const markNames = [
"bold", "bold",

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
module ActionDispatch
module Http
# Normaliza los nombres de archivo para que se propaguen
# correctamente a través de todo el stack.
module UploadedFileDecorator
extend ActiveSupport::Concern
included do
# Devolver el nombre de archivo con caracteres unicode
# normalizados
def original_filename
@original_filename.unicode_normalize
end
end
end
end
end
ActionDispatch::Http::UploadedFile.include ActionDispatch::Http::UploadedFileDecorator

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
module ActiveStorage
module Attached::Changes::CreateOneDecorator
extend ActiveSupport::Concern
included do
private
# A partir de ahora todos los archivos se suben al servicio de
# cada sitio.
def attachment_service_name
record.name.to_sym
end
end
end
end
ActiveStorage::Attached::Changes::CreateOne.include ActiveStorage::Attached::Changes::CreateOneDecorator

View file

@ -0,0 +1,82 @@
# frozen_string_literal: true
module ActiveStorage
class Service
# Sube los archivos a cada repositorio y los agrega al LFS de su
# repositorio git.
#
# @todo: Implementar LFS. No nos gusta mucho la idea porque duplica
# el espacio en disco, pero es la única forma que tenemos (hasta que
# implementemos IPFS) para poder transferir los archivos junto con el
# sitio.
class JekyllService < Service::DiskService
# Genera un servicio para un sitio determinado
#
# @param :site [Site]
# @return [ActiveStorage::Service::JekyllService]
def self.build_for_site(site:)
new(root: File.join(site.path, 'public'), public: true).tap do |js|
js.name = site.name.to_sym
end
end
# Lo mismo que en DiskService agregando el nombre de archivo en la
# firma. Esto permite que luego podamos guardar el archivo donde
# corresponde.
#
# @param :key [String]
# @param :expires_in [Integer]
# @param :content_type [String]
# @param :content_length [Integer]
# @param :checksum [String]
# @return [String]
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
instrument :url, key: key do |payload|
verified_token_with_expiration = ActiveStorage.verifier.generate(
{
key: key,
content_type: content_type,
content_length: content_length,
checksum: checksum,
service_name: name,
filename: filename_for(key)
},
expires_in: expires_in,
purpose: :blob_token
)
generated_url = url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: current_host)
payload[:url] = generated_url
generated_url
end
end
# Mantener retrocompatibilidad con cómo gestionamos los archivos
# subidos hasta ahora.
#
# @param :key [String]
# @return [String]
def folder_for(key)
key
end
# Obtiene el nombre de archivo para esta key
#
# @param :key [String]
# @return [String]
def filename_for(key)
ActiveStorage::Blob.where(key: key).limit(1).pluck(:filename).first
end
# Crea una ruta para la llave con un nombre conocido.
#
# @param :key [String]
# @return [String]
def path_for(key)
File.join root, folder_for(key), filename_for(key)
end
end
end
end

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
module ActiveStorage
class Service
# Modificaciones a ActiveStorage::Service::Registry
module RegistryDecorator
extend ActiveSupport::Concern
included do
# El mismo comportamiento que #fetch con el agregado de generar
# un {JekyllService} para cada sitio.
def fetch(name)
services.fetch(name.to_sym) do |key|
if configurations.include?(key)
services[key] = configurator.build(key)
elsif (site = Site.find_by_name(key))
services[key] = ActiveStorage::Service::JekyllService.build_for_site(site: site)
elsif block_given?
yield key
else
raise KeyError, "Missing configuration for the #{key} Active Storage service. " \
"Configurations available for the #{configurations.keys.to_sentence} services."
end
end
end
end
end
end
end
ActiveStorage::Service::Registry.include ActiveStorage::Service::RegistryDecorator

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
module ActiveStorage
# Modificaciones a ActiveStorage::Blob
module BlobDecorator
extend ActiveSupport::Concern
included do
# Permitir que llegue el nombre de archivo al servicio de subida de
# archivos.
#
# @return [Hash]
def service_metadata
if forcibly_serve_as_binary?
{ content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename }
elsif !allowed_inline?
{ content_type: content_type, disposition: :attachment, filename: filename }
else
{ content_type: content_type, filename: filename }
end
end
end
end
end
ActiveStorage::Blob.include ActiveStorage::BlobDecorator

View file

@ -13,12 +13,18 @@ class MetadataFile < MetadataTemplate
value == default_value value == default_value
end end
# No hay valores sugeridos para archivos subidos.
#
# XXX: Esto ayuda a deserializar en {Site#everything_of}
def values; end
def validate def validate
super super
errors << I18n.t("metadata.#{type}.site_invalid") if site.invalid? 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}.path_required") if path_missing?
errors << I18n.t("metadata.#{type}.no_file_for_description") if no_file_for_description? errors << I18n.t("metadata.#{type}.no_file_for_description") if no_file_for_description?
errors << I18n.t("metadata.#{type}.attachment_missing") if path? && !static_file
errors.compact! errors.compact!
errors.empty? errors.empty?
@ -34,12 +40,6 @@ class MetadataFile < MetadataTemplate
value['path'].is_a?(String) value['path'].is_a?(String)
end 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 # Asociar la imagen subida al sitio y obtener la ruta
# #
# XXX: Si evitamos guardar cambios con changed? no tenemos forma de # XXX: Si evitamos guardar cambios con changed? no tenemos forma de
@ -48,13 +48,7 @@ class MetadataFile < MetadataTemplate
# repetida. # repetida.
def save def save
value['description'] = sanitize value['description'] value['description'] = sanitize value['description']
value['path'] = static_file ? relative_destination_path_with_filename.to_s : nil
if path?
hardlink
value['path'] = relative_destination_path
else
value['path'] = nil
end
true true
end end
@ -71,101 +65,88 @@ class MetadataFile < MetadataTemplate
# XXX: La última opción provoca archivos duplicados, pero es lo mejor # XXX: La última opción provoca archivos duplicados, pero es lo mejor
# que tenemos hasta que resolvamos https://0xacab.org/sutty/sutty/-/issues/213 # que tenemos hasta que resolvamos https://0xacab.org/sutty/sutty/-/issues/213
# #
# @return [ActiveStorage::Attachment] # @todo encontrar una forma de obtener el attachment sin tener que
# recurrir al último subido.
#
# @return [ActiveStorage::Attachment,nil]
def static_file def static_file
return unless path?
@static_file ||= @static_file ||=
case value['path'] case value['path']
when ActionDispatch::Http::UploadedFile when ActionDispatch::Http::UploadedFile
site.static_files.last if site.static_files.attach(value['path']) site.static_files.last if site.static_files.attach(value['path'])
when String when String
if (blob = ActiveStorage::Blob.where(key: key_from_path).pluck(:id).first) if (blob_id = ActiveStorage::Blob.where(key: key_from_path).pluck(:id).first)
site.static_files.find_by(blob_id: blob) site.static_files.find_by(blob_id: blob_id)
elsif site.static_files.attach(io: path.open, filename: path.basename) elsif path? && pathname.exist? && site.static_files.attach(io: pathname.open, filename: pathname.basename)
site.static_files.last site.static_files.last.tap do |s|
s.blob.update(key: key_from_path)
end
end end
end end
end 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 def key_from_path
path.dirname.basename.to_s pathname.dirname.basename.to_s
end end
def path? def path?
value['path'].present? value['path'].present?
end end
def description?
value['description'].present?
end
private private
def filemagic # Obtener la ruta al archivo relativa al sitio
@filemagic ||= FileMagic.new(FileMagic::MAGIC_MIME) #
end
# @return [Pathname] # @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 def destination_path
@destination_path ||= File.join(site.path, relative_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)
value['path']
end
def relative_destination_path_with_filename
destination_path_with_filename.relative_path_from(Pathname.new(site.path).realpath)
end
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 end
# No hay archivo pero se lo describió # No hay archivo pero se lo describió
def no_file_for_description? def no_file_for_description?
value['description'].present? && value['path'].blank? !path? && description?
end end
end end

View file

@ -5,7 +5,7 @@ class MetadataImage < MetadataFile
def validate def validate
super super
errors << I18n.t('metadata.image.not_an_image') unless image? errors << I18n.t('metadata.image.not_an_image') if path? && !image?
errors.compact! errors.compact!
errors.empty? errors.empty?
@ -13,8 +13,6 @@ class MetadataImage < MetadataFile
# Determina si es una imagen # Determina si es una imagen
def image? def image?
return true unless file static_file&.blob&.send(:web_image?)
filemagic.file(file).starts_with? 'image/'
end end
end end

View file

@ -12,6 +12,6 @@ class MetadataMarkdown < MetadataText
# markdown y se eliminan autolinks. Mejor es habilitar la generación # markdown y se eliminan autolinks. Mejor es habilitar la generación
# SAFE de CommonMark en la configuración del sitio. # SAFE de CommonMark en la configuración del sitio.
def sanitize(string) def sanitize(string)
string string.unicode_normalize
end end
end end

View file

@ -25,6 +25,6 @@ class MetadataMarkdownContent < MetadataText
# markdown y se eliminan autolinks. Mejor es deshabilitar la # markdown y se eliminan autolinks. Mejor es deshabilitar la
# generación SAFE de CommonMark en la configuración del sitio. # generación SAFE de CommonMark en la configuración del sitio.
def sanitize(string) def sanitize(string)
string.tr("\r", '') string.tr("\r", '').unicode_normalize
end end
end end

View file

@ -19,7 +19,7 @@ class MetadataPermalink < MetadataString
# puntos suspensivos, la primera / para que siempre sea relativa y # puntos suspensivos, la primera / para que siempre sea relativa y
# agregamos una / al final si la ruta no tiene extensión. # agregamos una / al final si la ruta no tiene extensión.
def sanitize(value) def sanitize(value)
value = value.strip.gsub('..', '/').gsub('./', '').squeeze('/') value = value.strip.unicode_normalize.gsub('..', '/').gsub('./', '').squeeze('/')
value = value[1..-1] if value.start_with? '/' value = value[1..-1] if value.start_with? '/'
value += '/' if File.extname(value).blank? value += '/' if File.extname(value).blank?

View file

@ -17,7 +17,7 @@ class MetadataString < MetadataTemplate
def sanitize(string) def sanitize(string)
return '' if string.blank? return '' if string.blank?
sanitizer.sanitize(string.strip, sanitizer.sanitize(string.strip.unicode_normalize,
tags: [], tags: [],
attributes: []).strip.html_safe attributes: []).strip.html_safe
end end

View file

@ -184,9 +184,12 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
return if string.nil? return if string.nil?
return string unless string.is_a? String return string unless string.is_a? String
sanitizer.sanitize(string.tr("\r", ''), sanitizer
tags: allowed_tags, .sanitize(string.tr("\r", '').unicode_normalize,
attributes: allowed_attributes).strip.html_safe tags: allowed_tags,
attributes: allowed_attributes)
.strip
.html_safe
end end
def sanitizer def sanitizer
@ -199,7 +202,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
end end
def allowed_tags def allowed_tags
@allowed_tags ||= %w[strong em del u mark p h1 h2 h3 h4 h5 h6 ul ol li img iframe audio video div figure @allowed_tags ||= %w[strong em del u mark p h1 h2 h3 h4 h5 h6 ul ol li img iframe audio video div figure blockquote
figcaption a sub sup small].freeze figcaption a sub sup small].freeze
end end

View file

@ -57,7 +57,7 @@ class Site < ApplicationRecord
# Carga el sitio Jekyll una vez que se inicializa el modelo o después # Carga el sitio Jekyll una vez que se inicializa el modelo o después
# de crearlo # de crearlo
after_initialize :load_jekyll after_initialize :load_jekyll
after_create :load_jekyll, :static_file_migration! after_create :load_jekyll
# Cambiar el nombre del directorio # Cambiar el nombre del directorio
before_update :update_name! before_update :update_name!
before_save :add_private_key_if_missing! before_save :add_private_key_if_missing!
@ -474,11 +474,6 @@ class Site < ApplicationRecord
config.hostname = hostname config.hostname = hostname
end 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 # Valida si el sitio tiene al menos una forma de alojamiento asociada
# y es la local # y es la local
# #

View file

@ -1,52 +0,0 @@
# frozen_string_literal: true
class Site
# Obtiene todos los archivos relacionados en artículos del sitio y los
# sube a Sutty.
class StaticFileMigration
# Tipos de metadatos que contienen archivos
STATIC_TYPES = %i[file image].freeze
attr_reader :site
def initialize(site:)
@site = site
end
def migrate!
modified = site.docs.map do |doc|
next unless STATIC_TYPES.map do |field|
next unless doc.attribute? field
next unless doc[field].path?
next unless doc[field].static_file
true
end.any?
log.write "#{doc.path.relative};no se pudo guardar\n" unless doc.save(validate: false)
doc.path.absolute
end.compact
log.close
return if modified.empty?
# 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 ||= GitAuthor.new email: "sutty@#{Site.domain}",
name: 'Sutty'
end
def log
@log ||= File.open(File.join(site.path, 'migration.csv'), 'w')
end
end
end

View file

@ -95,6 +95,9 @@
%button.btn{ type: 'button', title: t('editor.right'), data: { editor_button: 'parentBlock-right' } }> %button.btn{ type: 'button', title: t('editor.right'), data: { editor_button: 'parentBlock-right' } }>
%i.fa.fa-fw.fa-align-right> %i.fa.fa-fw.fa-align-right>
%span.sr-only>= t('editor.right') %span.sr-only>= t('editor.right')
%button.btn{ type: 'button', title: t('editor.blockquote'), data: { editor_button: 'block-blockquote' } }>
%i.fa.fa-fw.fa-quote-left>
%span.sr-only>= t('editor.blockquote')
-# HAML cringe -# HAML cringe
.editor-auxiliary-toolbar.mt-1.scrollbar-black{ data: { editor_auxiliary_toolbar: '' } } .editor-auxiliary-toolbar.mt-1.scrollbar-black{ data: { editor_auxiliary_toolbar: '' } }

View file

@ -38,6 +38,12 @@ module Sutty
config.active_storage.variant_processor = :vips config.active_storage.variant_processor = :vips
config.to_prepare do
Dir.glob(File.join(File.dirname(__FILE__), '..', 'app', '**', '*_decorator.rb')).sort.each do |c|
Rails.configuration.cache_classes ? require(c) : load(c)
end
end
config.after_initialize do config.after_initialize do
ActiveStorage::DirectUploadsController.include ActiveStorage::AuthenticatedDirectUploadsController ActiveStorage::DirectUploadsController.include ActiveStorage::AuthenticatedDirectUploadsController

View file

@ -1,7 +0,0 @@
# 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

View file

@ -41,10 +41,12 @@ en:
not_an_image: 'Not an image' not_an_image: 'Not an image'
path_required: 'Missing image for upload' path_required: 'Missing image for upload'
no_file_for_description: "Description with no associated image" no_file_for_description: "Description with no associated image"
attachment_missing: "I couldn't save the image :("
file: file:
site_invalid: 'The file cannot be stored if the site configuration is not valid' site_invalid: 'The file cannot be stored if the site configuration is not valid'
path_required: "Missing file for upload" path_required: "Missing file for upload"
no_file_for_description: "Description with no associated file" no_file_for_description: "Description with no associated file"
attachment_missing: "I couldn't save the file :("
event: event:
zone_missing: 'Inexistent timezone' zone_missing: 'Inexistent timezone'
date_missing: 'Event date is required' date_missing: 'Event date is required'
@ -568,6 +570,7 @@ en:
left: Left left: Left
right: Right right: Right
center: Center center: Center
blockquote: Quote
color: Color color: Color
text-color: Text color text-color: Text color
multimedia: Media multimedia: Media

View file

@ -41,10 +41,12 @@ es:
not_an_image: 'No es una imagen' not_an_image: 'No es una imagen'
path_required: 'Se necesita una imagen' path_required: 'Se necesita una imagen'
no_file_for_description: 'Se envió una descripción sin imagen asociada' no_file_for_description: 'Se envió una descripción sin imagen asociada'
attachment_missing: 'no pude guardar el archivo :('
file: file:
site_invalid: 'El archivo no se puede almacenar si la configuración del sitio no es válida' site_invalid: 'El archivo no se puede almacenar si la configuración del sitio no es válida'
path_required: 'Se necesita un archivo' path_required: 'Se necesita un archivo'
no_file_for_description: 'se envió una descripción sin archivo asociado' no_file_for_description: 'se envió una descripción sin archivo asociado'
attachment_missing: 'no pude guardar el archivo :('
event: event:
zone_missing: 'El huso horario no es correcto' zone_missing: 'El huso horario no es correcto'
date_missing: 'La fecha es obligatoria' date_missing: 'La fecha es obligatoria'
@ -576,6 +578,7 @@ es:
left: Izquierda left: Izquierda
right: Derecha right: Derecha
center: Centro center: Centro
blockquote: Cita
color: Color color: Color
text-color: Color del texto text-color: Color del texto
multimedia: Multimedia multimedia: Multimedia

View file

@ -1,10 +1,38 @@
#!/bin/sh #!/bin/sh
set -e set -e
s_pid=/srv/tmp/puma.pid
p_pid=/tmp/prometheus.pid
case $1 in case $1 in
sutty) start)
su app -c "cd /srv/http && foreman start migrate" su rails -c "cd /srv && foreman run migrate"
daemonize -c /srv/http -u app /usr/bin/foreman start sutty daemonize -c /srv -u rails /usr/bin/foreman start sutty
;;
stop)
cat $s_pid | xargs -r kill
;;
reload)
cat $s_pid | xargs -r kill -USR2
;;
prometheus)
case $2 in
start)
rm -f $p_pid
daemonize -c /srv -p $p_pid -l $p_pid -u rails /usr/bin/foreman start prometheus
;;
stop)
cat $p_pid | xargs -r kill
rm -f $p_pid
;;
esac
;;
blazer)
test -z "$2" || b="_$2"
su rails -c "cd /srv && foreman run blazer$b"
;; ;;
prometheus) daemonize -c /srv/http -p /tmp/prometheus.pid -l /tmp/prometheus.pid -u app /usr/bin/foreman start prometheus ;;
esac esac

View file

@ -1,31 +1,27 @@
check process sutty with pidfile /srv/http/tmp/puma.pid check process sutty with pidfile /srv/tmp/puma.pid
start program = "/usr/local/bin/sutty sutty" start program = "/usr/local/bin/sutty start"
stop program = "/bin/sh -c 'cat /srv/http/tmp/puma.pid | xargs kill'" stop program = "/usr/local/bin/sutty stop"
check process prometheus with pidfile /tmp/prometheus.pid check process prometheus with pidfile /tmp/prometheus.pid
start program = "/usr/local/bin/sutty prometheus" start program = "/usr/local/bin/sutty prometheus start"
stop program = "/bin/sh -c 'cat /tmp/prometheus.pid | xargs kill'" stop program = "/usr/local/bin/sutty prometheus start"
check program blazer_5m check program blazer_5m
with path "/bin/sh -c 'cd /srv/http && foreman start blazer_5m'" with path "/usr/local/bin/sutty blazer 5m"
as uid "app" and gid "www-data"
every 5 cycles every 5 cycles
if status != 0 then alert if status != 0 then alert
check program blazer_1h check program blazer_1h
with path "/bin/sh -c 'cd /srv/http && foreman start blazer_1h'" with path "/usr/local/bin/sutty blazer 1h"
as uid "app" and gid "www-data"
every 60 cycles every 60 cycles
if status != 0 then alert if status != 0 then alert
check program blazer_1d check program blazer_1d
with path "/bin/sh -c 'cd /srv/http && foreman start blazer_1d'" with path "/usr/local/bin/sutty blazer 1d"
as uid "app" and gid "www-data"
every 1440 cycles every 1440 cycles
if status != 0 then alert if status != 0 then alert
check program blazer check program blazer
with path "/bin/sh -c 'cd /srv/http && foreman start blazer'" with path "/usr/local/bin/sutty blazer"
as uid "app" and gid "www-data"
every 61 cycles every 61 cycles
if status != 0 then alert if status != 0 then alert