5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-23 06:56:22 +00:00

Merge branch 'rails' into usar-sutty-editor

This commit is contained in:
f 2022-10-26 17:24:27 -03:00
commit fda9047590
70 changed files with 1587 additions and 1036 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,5 +1,8 @@
RAILS_GROUPS=assets
DELEGATE=athshe.sutty.nl
HAINISH=../haini.sh/haini.sh
DATABASE= DATABASE=
RAILS_ENV= RAILS_ENV=development
IMAP_SERVER= IMAP_SERVER=
DEFAULT_FROM= DEFAULT_FROM=
EXCEPTION_TO= EXCEPTION_TO=

View file

@ -1,128 +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.5 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-bundler ruby-json ruby-bigdecimal ruby-rake
RUN apk add --no-cache postgresql-libs git yarn brotli libssh2 python3
RUN test "2.7.3" = `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
# 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 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/
# Eliminar la necesidad de un runtime JS en producción, porque los
# assets ya están pre-compilados.
RUN sed -re "/(sassc|uglifier|bootstrap|coffee-rails)/d" -i Gemfile
RUN bundle clean
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 sutty/monit:latest
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-bundler ruby-json ruby-bigdecimal ruby-rake ruby-irb #
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.3" = `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 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

22
Gemfile
View file

@ -11,13 +11,18 @@ gem 'dotenv-rails', require: 'dotenv/rails-now'
gem 'rails', '~> 6' gem 'rails', '~> 6'
# Use Puma as the app server # Use Puma as the app server
gem 'puma' gem 'puma'
# See https://github.com/rails/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby # Solo incluir las gemas cuando estemos en desarrollo o compilando los
# Use SCSS for stylesheets # assets. No es necesario instalarlas en producción.
gem 'sassc-rails' #
# Use Uglifier as compressor for JavaScript assets # XXX: Supuestamente Rails ya soporta RAILS_GROUPS, pero Bundler no.
gem 'uglifier', '>= 1.3.0' if ENV['RAILS_GROUPS']&.split(',')&.include? 'assets'
gem 'bootstrap', '~> 4' gem 'sassc-rails'
gem 'uglifier', '>= 1.3.0'
gem 'bootstrap', '~> 4'
end
gem 'nokogiri'
# Turbolinks makes navigating your web application faster. Read more: # Turbolinks makes navigating your web application faster. Read more:
# https://github.com/turbolinks/turbolinks # https://github.com/turbolinks/turbolinks
@ -28,6 +33,7 @@ gem 'jbuilder', '~> 2.5'
# Use ActiveModel has_secure_password # Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7' gem 'bcrypt', '~> 3.1.7'
gem 'blazer' gem 'blazer'
gem 'chartkick'
gem 'commonmarker' gem 'commonmarker'
gem 'devise' gem 'devise'
gem 'devise-i18n' gem 'devise-i18n'
@ -58,6 +64,7 @@ gem 'rails-i18n'
gem 'rails_warden' gem 'rails_warden'
gem 'redis', require: %w[redis redis/connection/hiredis] gem 'redis', require: %w[redis redis/connection/hiredis]
gem 'redis-rails' gem 'redis-rails'
gem 'rollups', git: 'https://github.com/ankane/rollup.git', branch: 'master'
gem 'rubyzip' gem 'rubyzip'
gem 'rugged' gem 'rugged'
gem 'concurrent-ruby-ext' gem 'concurrent-ruby-ext'
@ -67,6 +74,7 @@ gem 'terminal-table'
gem 'validates_hostname' gem 'validates_hostname'
gem 'webpacker' gem 'webpacker'
gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git' gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git'
gem 'kaminari'
# database # database
gem 'hairtrigger' gem 'hairtrigger'

View file

@ -6,6 +6,15 @@ GIT
rails (>= 3.0) rails (>= 3.0)
rake (>= 0.8.7) rake (>= 0.8.7)
GIT
remote: https://github.com/ankane/rollup.git
revision: 0ab6c603450175eb1004f7793e86486943cb9f72
branch: master
specs:
rollups (0.1.3)
activesupport (>= 5.1)
groupdate (>= 5.2)
GIT GIT
remote: https://github.com/fauno/email_address remote: https://github.com/fauno/email_address
revision: 536b51f7071b68a55140c0c1726b4cd401d1c04d revision: 536b51f7071b68a55140c0c1726b4cd401d1c04d
@ -18,66 +27,66 @@ GIT
GEM GEM
remote: https://gems.sutty.nl/ remote: https://gems.sutty.nl/
specs: specs:
actioncable (6.1.3.2) actioncable (6.1.4.1)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.1)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.3.2) actionmailbox (6.1.4.1)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.1)
activejob (= 6.1.3.2) activejob (= 6.1.4.1)
activerecord (= 6.1.3.2) activerecord (= 6.1.4.1)
activestorage (= 6.1.3.2) activestorage (= 6.1.4.1)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.1)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.3.2) actionmailer (6.1.4.1)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.1)
actionview (= 6.1.3.2) actionview (= 6.1.4.1)
activejob (= 6.1.3.2) activejob (= 6.1.4.1)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.1)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.1.3.2) actionpack (6.1.4.1)
actionview (= 6.1.3.2) actionview (= 6.1.4.1)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.1)
rack (~> 2.0, >= 2.0.9) rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.3.2) actiontext (6.1.4.1)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.1)
activerecord (= 6.1.3.2) activerecord (= 6.1.4.1)
activestorage (= 6.1.3.2) activestorage (= 6.1.4.1)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.3.2) actionview (6.1.4.1)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.1.3.2) activejob (6.1.4.1)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.3.2) activemodel (6.1.4.1)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.1)
activerecord (6.1.3.2) activerecord (6.1.4.1)
activemodel (= 6.1.3.2) activemodel (= 6.1.4.1)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.1)
activestorage (6.1.3.2) activestorage (6.1.4.1)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.1)
activejob (= 6.1.3.2) activejob (= 6.1.4.1)
activerecord (= 6.1.3.2) activerecord (= 6.1.4.1)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.1)
marcel (~> 1.0.0) marcel (~> 1.0.0)
mini_mime (~> 1.0.2) mini_mime (>= 1.1.0)
activesupport (6.1.3.2) activesupport (6.1.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
tzinfo (~> 2.0) tzinfo (~> 2.0)
zeitwerk (~> 2.3) zeitwerk (~> 2.3)
addressable (2.7.0) addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
adhesiones-jekyll-theme (0.2.1) adhesiones-jekyll-theme (0.2.1)
jekyll (~> 4.0) jekyll (~> 4.0)
@ -89,13 +98,13 @@ GEM
jekyll-relative-urls (~> 0.0) jekyll-relative-urls (~> 0.0)
jekyll-seo-tag (~> 2.1) jekyll-seo-tag (~> 2.1)
ast (2.4.2) ast (2.4.2)
autoprefixer-rails (10.2.5.0) autoprefixer-rails (10.3.3.0)
execjs (< 2.8.0) execjs (~> 2)
bcrypt (3.1.16-x86_64-linux-musl) bcrypt (3.1.16-x86_64-linux-musl)
bcrypt_pbkdf (1.1.0-x86_64-linux-musl) bcrypt_pbkdf (1.1.0-x86_64-linux-musl)
benchmark-ips (2.8.4) benchmark-ips (2.9.2)
bindex (0.8.1-x86_64-linux-musl) bindex (0.8.1-x86_64-linux-musl)
blazer (2.4.2) blazer (2.4.7)
activerecord (>= 5) activerecord (>= 5)
chartkick (>= 3.2) chartkick (>= 3.2)
railties (>= 5) railties (>= 5)
@ -104,7 +113,7 @@ GEM
autoprefixer-rails (>= 9.1.0) autoprefixer-rails (>= 9.1.0)
popper_js (>= 1.14.3, < 2) popper_js (>= 1.14.3, < 2)
sassc-rails (>= 2.0.0) sassc-rails (>= 2.0.0)
brakeman (5.0.1) brakeman (5.1.2)
builder (3.2.4) builder (3.2.4)
capybara (2.18.0) capybara (2.18.0)
addressable addressable
@ -113,15 +122,15 @@ GEM
rack (>= 1.0.0) rack (>= 1.0.0)
rack-test (>= 0.5.4) rack-test (>= 0.5.4)
xpath (>= 2.0, < 4.0) xpath (>= 2.0, < 4.0)
chartkick (4.0.4) chartkick (4.1.2)
childprocess (3.0.0) childprocess (4.1.0)
coderay (1.1.3) coderay (1.1.3)
colorator (1.1.0) colorator (1.1.0)
commonmarker (0.21.2-x86_64-linux-musl) commonmarker (0.21.2-x86_64-linux-musl)
ruby-enum (~> 0.5) ruby-enum (~> 0.5)
concurrent-ruby (1.1.8) concurrent-ruby (1.1.9)
concurrent-ruby-ext (1.1.8-x86_64-linux-musl) concurrent-ruby-ext (1.1.9-x86_64-linux-musl)
concurrent-ruby (= 1.1.8) concurrent-ruby (= 1.1.9)
crass (1.0.6) crass (1.0.6)
database_cleaner (2.0.1) database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0) database_cleaner-active_record (~> 2.0.0)
@ -129,8 +138,8 @@ GEM
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
dead_end (1.1.7) dead_end (3.1.0)
derailed_benchmarks (2.1.0) derailed_benchmarks (2.1.1)
benchmark-ips (~> 2) benchmark-ips (~> 2)
dead_end dead_end
get_process_mem (~> 0) get_process_mem (~> 0)
@ -148,8 +157,8 @@ GEM
railties (>= 4.1.0) railties (>= 4.1.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-i18n (1.9.4) devise-i18n (1.10.1)
devise (>= 4.7.1) devise (>= 4.8.0)
devise_invitable (2.0.5) devise_invitable (2.0.5)
actionmailer (>= 5.0) actionmailer (>= 5.0)
devise (>= 4.6) devise (>= 4.6)
@ -157,8 +166,8 @@ GEM
dotenv-rails (2.7.6) dotenv-rails (2.7.6)
dotenv (= 2.7.6) dotenv (= 2.7.6)
railties (>= 3.2) railties (>= 3.2)
down (5.2.1) down (5.2.4)
addressable (~> 2.5) addressable (~> 2.8)
ed25519 (1.2.4-x86_64-linux-musl) ed25519 (1.2.4-x86_64-linux-musl)
editorial-autogestiva-jekyll-theme (0.3.4) editorial-autogestiva-jekyll-theme (0.3.4)
jekyll (~> 4) jekyll (~> 4)
@ -179,48 +188,50 @@ GEM
jekyll-unique-urls (~> 0) jekyll-unique-urls (~> 0)
jekyll-write-and-commit-changes (~> 0) jekyll-write-and-commit-changes (~> 0)
sutty-liquid (~> 0) sutty-liquid (~> 0)
em-websocket (0.5.2) em-websocket (0.5.3)
eventmachine (>= 0.12.9) eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0) http_parser.rb (~> 0)
errbase (0.2.1) errbase (0.2.1)
erubi (1.10.0) erubi (1.10.0)
eventmachine (1.2.7-x86_64-linux-musl) eventmachine (1.2.7-x86_64-linux-musl)
exception_notification (4.4.3) exception_notification (4.4.3)
actionmailer (>= 4.0, < 7) actionmailer (>= 4.0, < 7)
activesupport (>= 4.0, < 7) activesupport (>= 4.0, < 7)
execjs (2.7.0) execjs (2.8.1)
factory_bot (6.2.0) factory_bot (6.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.2.0) factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0) factory_bot (~> 6.2.0)
railties (>= 5.0.0) railties (>= 5.0.0)
fast_blank (1.0.0-x86_64-linux-musl) fast_blank (1.0.1-x86_64-linux-musl)
fast_jsonparser (0.5.0-x86_64-linux-musl) fast_jsonparser (0.5.0-x86_64-linux-musl)
ffi (1.15.0-x86_64-linux-musl) ffi (1.15.4-x86_64-linux-musl)
flamegraph (0.9.5) flamegraph (0.9.5)
forwardable-extended (2.6.0) forwardable-extended (2.6.0)
friendly_id (5.4.2) friendly_id (5.4.2)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
get_process_mem (0.2.7) get_process_mem (0.2.7)
ffi (~> 1.0) ffi (~> 1.0)
globalid (0.4.2) globalid (0.6.0)
activesupport (>= 4.2.0) activesupport (>= 5.0)
groupdate (5.2.2)
activesupport (>= 5)
hairtrigger (0.2.24) hairtrigger (0.2.24)
activerecord (>= 5.0, < 7) activerecord (>= 5.0, < 7)
ruby2ruby (~> 2.4) ruby2ruby (~> 2.4)
ruby_parser (~> 3.10) ruby_parser (~> 3.10)
haml (5.2.1) haml (5.2.2)
temple (>= 0.8.0) temple (>= 0.8.0)
tilt tilt
haml-lint (0.999.999) haml-lint (0.999.999)
haml_lint haml_lint
haml_lint (0.37.0) haml_lint (0.37.1)
haml (>= 4.0, < 5.3) haml (>= 4.0, < 5.3)
parallel (~> 1.10) parallel (~> 1.10)
rainbow rainbow
rubocop (>= 0.50.0) rubocop (>= 0.50.0)
sysexits (~> 1.1) sysexits (~> 1.1)
hamlit (2.15.0-x86_64-linux-musl) hamlit (2.15.1-x86_64-linux-musl)
temple (>= 0.8.2) temple (>= 0.8.2)
thor thor
tilt tilt
@ -232,24 +243,24 @@ GEM
heapy (0.2.0) heapy (0.2.0)
thor thor
hiredis (0.6.3-x86_64-linux-musl) hiredis (0.6.3-x86_64-linux-musl)
http_parser.rb (0.6.0-x86_64-linux-musl) http_parser.rb (0.8.0-x86_64-linux-musl)
httparty (0.18.1) httparty (0.18.1)
mime-types (~> 3.0) mime-types (~> 3.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
i18n (1.8.10) i18n (1.8.11)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
icalendar (2.7.1) icalendar (2.7.1)
ice_cube (~> 0.16) ice_cube (~> 0.16)
ice_cube (0.16.3) ice_cube (0.16.4)
image_processing (1.12.1) image_processing (1.12.1)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
inline_svg (1.7.2) inline_svg (1.7.2)
activesupport (>= 3.0) activesupport (>= 3.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
jbuilder (2.11.2) jbuilder (2.11.3)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
jekyll (4.2.0) jekyll (4.2.1)
addressable (~> 2.4) addressable (~> 2.4)
colorator (~> 1.0) colorator (~> 1.0)
em-websocket (~> 0.5) em-websocket (~> 0.5)
@ -264,8 +275,8 @@ GEM
rouge (~> 3.0) rouge (~> 3.0)
safe_yaml (~> 1.0) safe_yaml (~> 1.0)
terminal-table (~> 2.0) terminal-table (~> 2.0)
jekyll-commonmark (1.3.1) jekyll-commonmark (1.3.2)
commonmarker (~> 0.14) commonmarker (~> 0.14, < 0.22)
jekyll (>= 3.7, < 5.0) jekyll (>= 3.7, < 5.0)
jekyll-data (1.1.2) jekyll-data (1.1.2)
jekyll (>= 3.3, < 5.0.0) jekyll (>= 3.3, < 5.0.0)
@ -276,21 +287,19 @@ GEM
jekyll (>= 3.7, < 5.0) jekyll (>= 3.7, < 5.0)
jekyll-hardlinks (0.1.2) jekyll-hardlinks (0.1.2)
jekyll (~> 4) jekyll (~> 4)
jekyll-ignore-layouts (0.1.0) jekyll-ignore-layouts (0.1.2)
jekyll (~> 4) jekyll (~> 4)
jekyll-images (0.2.7) jekyll-images (0.3.0)
jekyll (~> 4) jekyll (~> 4)
ruby-filemagic (~> 0.7) ruby-filemagic (~> 0.7)
ruby-vips (~> 2) ruby-vips (~> 2)
jekyll-include-cache (0.2.1) jekyll-include-cache (0.2.1)
jekyll (>= 3.7, < 5.0) jekyll (>= 3.7, < 5.0)
jekyll-linked-posts (0.2.0) jekyll-linked-posts (0.4.2)
jekyll (~> 4) jekyll (~> 4)
jekyll-locales (0.1.12) jekyll-locales (0.1.13)
jekyll-lunr (0.2.0) jekyll-lunr (0.3.0)
loofah (~> 2.4) loofah (~> 2.4)
jekyll-node-modules (0.1.0)
jekyll (~> 4)
jekyll-order (0.1.4) jekyll-order (0.1.4)
jekyll-relative-urls (0.0.6) jekyll-relative-urls (0.0.6)
jekyll (~> 4) jekyll (~> 4)
@ -298,9 +307,9 @@ GEM
sassc (> 2.0.1, < 3.0) sassc (> 2.0.1, < 3.0)
jekyll-seo-tag (2.7.1) jekyll-seo-tag (2.7.1)
jekyll (>= 3.8, < 5.0) jekyll (>= 3.8, < 5.0)
jekyll-spree-client (0.1.14) jekyll-spree-client (0.1.19)
fast_blank (~> 1) fast_blank (~> 1)
spree-api-client (~> 0.2) spree-api-client (>= 0.2.4)
jekyll-turbolinks (0.0.5) jekyll-turbolinks (0.0.5)
jekyll (~> 4) jekyll (~> 4)
turbolinks-source (~> 5) turbolinks-source (~> 5)
@ -308,9 +317,21 @@ GEM
jekyll (~> 4) jekyll (~> 4)
jekyll-watch (2.2.1) jekyll-watch (2.2.1)
listen (~> 3.0) listen (~> 3.0)
jekyll-write-and-commit-changes (0.1.2) jekyll-write-and-commit-changes (0.2.1)
jekyll (~> 4) jekyll (~> 4)
rugged (~> 1) rugged (~> 1)
kaminari (1.2.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1)
kaminari-activerecord (= 1.2.1)
kaminari-core (= 1.2.1)
kaminari-actionview (1.2.1)
actionview
kaminari-core (= 1.2.1)
kaminari-activerecord (1.2.1)
activerecord
kaminari-core (= 1.2.1)
kaminari-core (1.2.1)
kramdown (2.3.1) kramdown (2.3.1)
rexml rexml
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
@ -326,46 +347,46 @@ GEM
ruby_dep (~> 1.2) ruby_dep (~> 1.2)
loaf (0.10.0) loaf (0.10.0)
railties (>= 3.2) railties (>= 3.2)
lockbox (0.6.4) lockbox (0.6.6)
lograge (0.11.2) lograge (0.11.2)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.9.1) loofah (2.12.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.1) mail (2.7.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
marcel (1.0.1) marcel (1.0.2)
memory_profiler (1.0.0) memory_profiler (1.0.0)
mercenary (0.4.0) mercenary (0.4.0)
method_source (1.0.0) method_source (1.0.0)
mime-types (3.3.1) mime-types (3.4.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2021.0225) mime-types-data (3.2021.1115)
mini_histogram (0.3.1) mini_histogram (0.3.1)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.0.3) mini_mime (1.1.2)
mini_portile2 (2.5.1) mini_portile2 (2.6.1)
minima (2.5.1) minima (2.5.1)
jekyll (>= 3.5, < 5.0) jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9) jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1) jekyll-seo-tag (~> 2.1)
minitest (5.14.4) minitest (5.14.4)
mobility (1.1.2) mobility (1.2.4)
i18n (>= 0.6.10, < 2) i18n (>= 0.6.10, < 2)
request_store (~> 1.0) request_store (~> 1.0)
multi_xml (0.6.0) multi_xml (0.6.0)
net-ssh (6.1.0) net-ssh (6.1.0)
netaddr (2.0.4) netaddr (2.0.5)
nio4r (2.5.7-x86_64-linux-musl) nio4r (2.5.8-x86_64-linux-musl)
nokogiri (1.11.5-x86_64-linux-musl) nokogiri (1.12.5-x86_64-linux-musl)
mini_portile2 (~> 2.5.0) mini_portile2 (~> 2.6.1)
racc (~> 1.4) racc (~> 1.4)
orm_adapter (0.5.0) orm_adapter (0.5.0)
parallel (1.20.1) parallel (1.21.0)
parser (3.0.1.1) parser (3.0.2.0)
ast (~> 2.4.1) ast (~> 2.4.1)
pathutil (0.16.2) pathutil (0.16.2)
forwardable-extended (~> 2.6) forwardable-extended (~> 2.6)
@ -374,27 +395,27 @@ GEM
activerecord (>= 5.2) activerecord (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
popper_js (1.16.0) popper_js (1.16.0)
prometheus_exporter (0.7.0) prometheus_exporter (1.0.0)
webrick webrick
pry (0.14.1) pry (0.14.1)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
public_suffix (4.0.6) public_suffix (4.0.6)
puma (5.3.1-x86_64-linux-musl) puma (5.5.2-x86_64-linux-musl)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.1.0) pundit (2.1.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
racc (1.5.2-x86_64-linux-musl) racc (1.6.0-x86_64-linux-musl)
rack (2.2.3) rack (2.2.3)
rack-cors (1.1.1) rack-cors (1.1.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-mini-profiler (2.3.2) rack-mini-profiler (2.3.3)
rack (>= 1.2.0) rack (>= 1.2.0)
rack-proxy (0.6.5) rack-proxy (0.7.0)
rack rack
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
radios-comunitarias-jekyll-theme (0.1.4) radios-comunitarias-jekyll-theme (0.1.5)
jekyll (~> 4.0) jekyll (~> 4.0)
jekyll-data (~> 1.1) jekyll-data (~> 1.1)
jekyll-feed (~> 0.9) jekyll-feed (~> 0.9)
@ -405,65 +426,66 @@ GEM
jekyll-relative-urls (~> 0.0) jekyll-relative-urls (~> 0.0)
jekyll-seo-tag (~> 2.1) jekyll-seo-tag (~> 2.1)
jekyll-turbolinks (~> 0) jekyll-turbolinks (~> 0)
rails (6.1.3.2) rails (6.1.4.1)
actioncable (= 6.1.3.2) actioncable (= 6.1.4.1)
actionmailbox (= 6.1.3.2) actionmailbox (= 6.1.4.1)
actionmailer (= 6.1.3.2) actionmailer (= 6.1.4.1)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.1)
actiontext (= 6.1.3.2) actiontext (= 6.1.4.1)
actionview (= 6.1.3.2) actionview (= 6.1.4.1)
activejob (= 6.1.3.2) activejob (= 6.1.4.1)
activemodel (= 6.1.3.2) activemodel (= 6.1.4.1)
activerecord (= 6.1.3.2) activerecord (= 6.1.4.1)
activestorage (= 6.1.3.2) activestorage (= 6.1.4.1)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.3.2) railties (= 6.1.4.1)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0) rails-html-sanitizer (1.4.2)
loofah (~> 2.3) loofah (~> 2.3)
rails-i18n (6.0.0) rails-i18n (6.0.0)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7) railties (>= 6.0.0, < 7)
rails_warden (0.6.0) rails_warden (0.6.0)
warden (>= 1.2.0) warden (>= 1.2.0)
railties (6.1.3.2) railties (6.1.4.1)
actionpack (= 6.1.3.2) actionpack (= 6.1.4.1)
activesupport (= 6.1.3.2) activesupport (= 6.1.4.1)
method_source method_source
rake (>= 0.8.7) rake (>= 0.13)
thor (~> 1.0) thor (~> 1.0)
rainbow (3.0.0) rainbow (3.0.0)
rake (13.0.3) rake (13.0.6)
rb-fsevent (0.11.0) rb-fsevent (0.11.0)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
recursero-jekyll-theme (0.1.3) recursero-jekyll-theme (0.2.0)
jekyll (~> 4.0) jekyll (~> 4)
jekyll-commonmark (~> 1.3)
jekyll-data (~> 1.1) jekyll-data (~> 1.1)
jekyll-feed (~> 0.9) jekyll-dotenv (>= 0.2)
jekyll-feed (~> 0.15)
jekyll-ignore-layouts (~> 0)
jekyll-images (~> 0.2) jekyll-images (~> 0.2)
jekyll-include-cache (~> 0) jekyll-include-cache (~> 0)
jekyll-linked-posts (~> 0.2) jekyll-linked-posts (~> 0)
jekyll-locales (~> 0.1) jekyll-locales (~> 0.1)
jekyll-lunr (~> 0.1) jekyll-lunr (~> 0.1)
jekyll-node-modules (~> 0.1) jekyll-order (~> 0)
jekyll-order (~> 0.1) jekyll-relative-urls (~> 0)
jekyll-relative-urls (~> 0.0) jekyll-seo-tag (~> 2)
jekyll-seo-tag (~> 2.1)
jekyll-turbolinks (~> 0)
jekyll-unique-urls (~> 0.1) jekyll-unique-urls (~> 0.1)
sutty-archives (~> 2.2) sutty-archives (~> 2.2)
sutty-liquid (~> 0.1) sutty-liquid (~> 0)
redis (4.2.5) redis (4.5.1)
redis-actionpack (5.2.0) redis-actionpack (5.2.0)
actionpack (>= 5, < 7) actionpack (>= 5, < 7)
redis-rack (>= 2.1.0, < 3) redis-rack (>= 2.1.0, < 3)
redis-store (>= 1.1.0, < 2) redis-store (>= 1.1.0, < 2)
redis-activesupport (5.2.0) redis-activesupport (5.2.1)
activesupport (>= 3, < 7) activesupport (>= 3, < 7)
redis-store (>= 1.3, < 2) redis-store (>= 1.3, < 2)
redis-rack (2.1.3) redis-rack (2.1.3)
@ -482,19 +504,19 @@ GEM
actionpack (>= 5.0) actionpack (>= 5.0)
railties (>= 5.0) railties (>= 5.0)
rexml (3.2.5) rexml (3.2.5)
rouge (3.26.0) rouge (3.26.1)
rubocop (1.15.0) rubocop (1.23.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.0.0.0) parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml rexml
rubocop-ast (>= 1.5.0, < 2.0) rubocop-ast (>= 1.12.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0) unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.5.0) rubocop-ast (1.13.0)
parser (>= 3.0.1.1) parser (>= 3.0.1.1)
rubocop-rails (2.10.1) rubocop-rails (2.12.4)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
@ -502,17 +524,17 @@ GEM
i18n i18n
ruby-filemagic (0.7.2-x86_64-linux-musl) ruby-filemagic (0.7.2-x86_64-linux-musl)
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
ruby-statistics (2.1.3) ruby-statistics (3.0.0)
ruby-vips (2.1.2) ruby-vips (2.1.4)
ffi (~> 1.12) ffi (~> 1.12)
ruby2ruby (2.4.4) ruby2ruby (2.4.4)
ruby_parser (~> 3.1) ruby_parser (~> 3.1)
sexp_processor (~> 4.6) sexp_processor (~> 4.6)
ruby_dep (1.5.0) ruby_dep (1.5.0)
ruby_parser (3.15.1) ruby_parser (3.18.1)
sexp_processor (~> 4.9) sexp_processor (~> 4.16)
rubyzip (2.3.0) rubyzip (2.3.2)
rugged (1.1.0-x86_64-linux-musl) rugged (1.2.0-x86_64-linux-musl)
safe_yaml (1.0.6) safe_yaml (1.0.6)
safely_block (0.3.0) safely_block (0.3.0)
errbase (>= 0.1.1) errbase (>= 0.1.1)
@ -524,11 +546,12 @@ GEM
sprockets (> 3.0) sprockets (> 3.0)
sprockets-rails sprockets-rails
tilt tilt
selenium-webdriver (3.142.7) selenium-webdriver (4.1.0)
childprocess (>= 0.5, < 4.0) childprocess (>= 0.5, < 5.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2) rubyzip (>= 1.2.2)
semantic_range (3.0.0) semantic_range (3.0.0)
sexp_processor (4.15.2) sexp_processor (4.16.0)
share-to-fediverse-jekyll-theme (0.1.4) share-to-fediverse-jekyll-theme (0.1.4)
jekyll (~> 4.0) jekyll (~> 4.0)
jekyll-data (~> 1.1) jekyll-data (~> 1.1)
@ -540,7 +563,7 @@ GEM
simpleidn (0.2.1) simpleidn (0.2.1)
unf (~> 0.1.4) unf (~> 0.1.4)
sourcemap (0.1.1) sourcemap (0.1.1)
spree-api-client (0.2.1) spree-api-client (0.2.4)
fast_blank (~> 1) fast_blank (~> 1)
httparty (~> 0.18.0) httparty (~> 0.18.0)
spring (2.1.1) spring (2.1.1)
@ -550,9 +573,9 @@ GEM
sprockets (4.0.2) sprockets (4.0.2)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-rails (3.2.2) sprockets-rails (3.4.1)
actionpack (>= 4.0) actionpack (>= 5.2)
activesupport (>= 4.0) activesupport (>= 5.2)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sqlite3 (1.4.2-x86_64-linux-musl) sqlite3 (1.4.2-x86_64-linux-musl)
stackprof (0.2.17-x86_64-linux-musl) stackprof (0.2.17-x86_64-linux-musl)
@ -577,14 +600,14 @@ GEM
jekyll-include-cache (~> 0) jekyll-include-cache (~> 0)
jekyll-relative-urls (~> 0.0) jekyll-relative-urls (~> 0.0)
jekyll-seo-tag (~> 2.1) jekyll-seo-tag (~> 2.1)
sutty-liquid (0.7.3) sutty-liquid (0.7.4)
fast_blank (~> 1.0) fast_blank (~> 1.0)
jekyll (~> 4) jekyll (~> 4)
sutty-minima (2.5.0) sutty-minima (2.5.0)
jekyll (>= 3.5, < 5.0) jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9) jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1) jekyll-seo-tag (~> 2.1)
symbol-fstring (1.0.0-x86_64-linux-musl) symbol-fstring (1.0.2-x86_64-linux-musl)
sysexits (1.2.0) sysexits (1.2.0)
temple (0.8.2) temple (0.8.2)
terminal-table (2.0.0) terminal-table (2.0.0)
@ -601,30 +624,30 @@ GEM
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.7-x86_64-linux-musl) unf_ext (0.0.8-x86_64-linux-musl)
unicode-display_width (1.7.0) unicode-display_width (1.8.0)
validates_hostname (1.0.11) validates_hostname (1.0.11)
activerecord (>= 3.0) activerecord (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.1.0) web-console (4.2.0)
actionview (>= 6.0.0) actionview (>= 6.0.0)
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webpacker (5.4.0) webpacker (5.4.3)
activesupport (>= 5.2) activesupport (>= 5.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
webrick (1.7.0) webrick (1.7.0)
websocket-driver (0.7.3-x86_64-linux-musl) websocket-driver (0.7.5-x86_64-linux-musl)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.4.2) zeitwerk (2.5.1)
PLATFORMS PLATFORMS
ruby ruby
@ -638,6 +661,7 @@ DEPENDENCIES
bootstrap (~> 4) bootstrap (~> 4)
brakeman brakeman
capybara (~> 2.13) capybara (~> 2.13)
chartkick
commonmarker commonmarker
concurrent-ruby-ext concurrent-ruby-ext
database_cleaner database_cleaner
@ -670,6 +694,7 @@ DEPENDENCIES
jekyll-data! jekyll-data!
jekyll-images jekyll-images
jekyll-include-cache jekyll-include-cache
kaminari
letter_opener letter_opener
listen (>= 3.0.5, < 3.2) listen (>= 3.0.5, < 3.2)
loaf loaf
@ -680,6 +705,7 @@ DEPENDENCIES
minima minima
mobility mobility
net-ssh net-ssh
nokogiri
pg pg
pg_search pg_search
prometheus_exporter prometheus_exporter
@ -695,6 +721,7 @@ DEPENDENCIES
recursero-jekyll-theme recursero-jekyll-theme
redis redis
redis-rails redis-rails
rollups!
rubocop-rails rubocop-rails
rubyzip rubyzip
rugged rugged

202
Makefile
View file

@ -1,145 +1,137 @@
.SHELL := /bin/bash SHELL := /bin/bash
# Incluir las variables de entorno .DEFAULT_GOAL := help
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
root_dir := $(patsubst %/,%,$(dir $(mkfile_path)))
include $(root_dir)/.env
delegate := athshe # Copiar el archivo de configuración y avisar cuando hay que
# actualizarlo.
.env: .env.example
@test -f $@ || cp -v $< $@
@test -f $@ && echo "Revisa $@ para actualizarlo con respecto a $<"
@test -f $@ && diff -auN --color $@ $<
assets := package.json yarn.lock $(shell find app/assets/ app/javascript/ -type f) include .env
alpine_version := 3.13 export
hain ?= ../haini.sh/haini.sh
env ?= staging # XXX: El espacio antes del comentario cuenta como espacio
args ?=## Argumentos para Hain
commit ?= origin/rails## Commit desde el que actualizar
env ?= staging## Entorno del nodo delegado
sutty ?= $(SUTTY)## Dirección local
delegate ?= $(DELEGATE)## Cambia el nodo delegado
hain ?= ENV_FILE=.env $(HAINISH)## Ubicación de Hainish
# El nodo delegado tiene dos entornos, production y staging.
# Dependiendo del entorno que elijamos, se van a generar los assets y el
# contenedor y subirse a un servidor u otro. No utilizamos CI/CD (aún).
#
# Production es el entorno de panel.sutty.nl
ifeq ($(env),production) ifeq ($(env),production)
container ?= sutty container ?= panel
## TODO: Cambiar a otra cosa
branch ?= rails branch ?= rails
public ?= public public ?= public
endif endif
# Staging es el entorno de panel.staging.sutty.nl
ifeq ($(env),staging) ifeq ($(env),staging)
container := staging container := staging
branch := staging branch := staging
public := staging public := staging
endif endif
export help: always ## Ayuda
@echo -e "Sutty\n" | sed -re "s/^.*/\x1B[38;5;197m&\x1B[0m/"
@echo -e "Servidor: https://panel.$(SUTTY_WITH_PORT)/\n"
@echo -e "Uso: make TAREA args=\"ARGUMENTOS\"\n"
@echo -e "Tareas:\n"
@grep -E "^[a-z\-]+:.*##" Makefile | sed -re "s/(.*):.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/"
@echo -e "\nArgumentos:\n"
@grep -E "^[a-z\-]+ \?=.*##" Makefile | sed -re "s/(.*) \?=.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/"
public/packs/manifest.json.br: $(assets) assets: public/packs/manifest.json.br ## Compilar los assets
$(hain) 'cd /Sutty/sutty; PANEL_URL=https://panel.sutty.nl RAILS_ENV=production NODE_ENV=production bundle exec rake assets:precompile assets:clean'
assets: public/packs/manifest.json.br test: always ## Ejecutar los tests
$(MAKE) rake args="test RAILS_ENV=test $(args)"
tests := $(shell find test/ -name "*_test.rb") postgresql: /etc/hosts ## Iniciar la base de datos
$(tests): always
$(hain) 'cd /Sutty/sutty; bundle exec rake test TEST="$@" RAILS_ENV=test'
test: always
$(hain) 'cd /Sutty/sutty; RAILS_ENV=test bundle exec rake test'
postgresql: /etc/hosts
pgrep postgres >/dev/null || $(hain) postgresql pgrep postgres >/dev/null || $(hain) postgresql
serve: /etc/hosts postgresql serve-js: /etc/hosts node_modules ## Iniciar el servidor de desarrollo de Javascript
$(hain) 'bundle exec ./bin/webpack-dev-server'
serve: /etc/hosts postgresql Gemfile.lock ## Iniciar el servidor de desarrollo de Rails
$(MAKE) rails args=server $(MAKE) rails args=server
# make rails args="db:migrate" rails: ## Corre rails dentro del entorno de desarrollo (pasar argumentos con args=).
rails:
$(MAKE) bundle args="exec rails $(args)" $(MAKE) bundle args="exec rails $(args)"
rake: rake: ## Corre rake dentro del entorno de desarrollo (pasar argumentos con args=).
$(MAKE) bundle args="exec rake $(args)" $(MAKE) bundle args="exec rake $(args)"
bundle: bundle: ## Corre bundle dentro del entorno de desarrollo (pasar argumentos con args=).
$(hain) 'cd /Sutty/sutty; bundle $(args)' $(hain) 'bundle $(args)'
yarn: psql := psql -h $(PG_HOST) -U $(PG_USER) -p $(PG_PORT) -d sutty
copy-table:
test -n "$(table)"
echo "truncate $(table) $(cascade);" | $(psql)
ssh $(delegate) docker exec postgresql pg_dump -U sutty -d sutty -t $(table) | $(psql)
psql:
$(psql)
rubocop: ## Yutea el código que está por ser commiteado
git status --porcelain \
| grep -E "^(A|M)" \
| sed "s/^...//" \
| grep ".rb$$" \
| ../haini.sh/haini.sh "xargs -r ./bin/rubocop --auto-correct"
audit: ## Encuentra dependencias con vulnerabilidades
$(hain) 'gem install bundler-audit'
$(hain) 'bundle audit --update'
brakeman: ## Busca posibles vulnerabilidades en Sutty
$(MAKE) bundle args='exec brakeman'
yarn: ## Tareas de yarn
$(hain) 'yarn $(args)' $(hain) 'yarn $(args)'
# Servir JS con el dev server. clean: ## Limpieza
# Esto acelera la compilación del javascript, tiene que correrse por separado rm -rf _sites/test-* _deploy/test-* log/*.log tmp/cache tmp/letter_opener tmp/miniprofiler tmp/storage
# de serve.
serve-js: /etc/hosts
$(hain) 'cd /Sutty/sutty; bundle exec ./bin/webpack-dev-server'
# Limpiar los archivos de testeo build: Gemfile.lock ## Generar la imagen Docker
clean:
rm -rf _sites/test-* _deploy/test-*
# Generar la imagen Docker
build: assets
time docker build --build-arg="BRANCH=$(branch)" --build-arg="RAILS_MASTER_KEY=`cat config/master.key`" -t sutty/$(container) . time docker build --build-arg="BRANCH=$(branch)" --build-arg="RAILS_MASTER_KEY=`cat config/master.key`" -t sutty/$(container) .
docker tag sutty/$(container):latest sutty:keep docker tag sutty/$(container):latest sutty:keep
@echo -e "\a"
save: save: ## Subir la imagen Docker al nodo delegado
time docker save sutty/$(container):latest | ssh root@$(delegate).sutty.nl docker load time docker save sutty/$(container):latest | ssh root@$(delegate) docker load
date +%F | xargs -I {} git tag -f $(container)-{} date +%F | xargs -I {} git tag -f $(container)-{}
@echo -e "\a" @echo -e "\a"
# proyectos. ota-js: assets ## Actualizar Javascript en el nodo delegado
../gems/: rsync -avi --delete-after --chown 1000:82 public/ root@$(delegate):/srv/sutty/srv/http/data/_$(public)/
mkdir -p $@ ssh root@$(delegate) docker exec $(container) sh -c "cat /srv/http/tmp/puma.pid | xargs -r kill -USR2"
# Crear el directorio donde se almacenan las gemas binarias ota: ## Actualizar Rails en el nodo delegado
# TODO: Mover a un proyecto propio, porque lo utilizamos en todos los ssh $(delegate) git -C /srv/sutty/srv/http/panel.sutty.nl pull ; true
gem_dir := $(shell readlink -f ../gems) ssh $(delegate) chown -R 1000:82 /srv/sutty/srv/http/panel.sutty.nl
gem_cache_dir := $(gem_dir)/cache ssh $(delegate) docker exec $(container) rails reload
gem_binary_dir := $(gem_dir)/$(alpine_version)
ifeq ($(MAKECMDGOALS),build-gems)
gems := $(shell bundle show --paths | xargs -I {} sh -c 'find {}/ext/ -name extconf.rb &>/dev/null && basename {}')
gems := $(patsubst %-x86_64-linux,%,$(gems))
gems := $(patsubst %,$(gem_cache_dir)/%.gem,$(gems))
gems_musl := $(patsubst $(gem_cache_dir)/%.gem,$(gem_binary_dir)/%-x86_64-linux-musl.gem,$(gems))
endif
$(gem_binary_dir)/%-x86_64-linux-musl.gem: # Todos los archivos de assets. Si alguno cambia, se van a recompilar
@docker run \ # los assets que luego se suben al nodo delegado.
-v $(gem_dir):/srv/gems \ assets := package.json yarn.lock $(shell find app/assets/ app/javascript/ -type f)
-v `readlink -f ~/.ccache`:/home/builder/.ccache \ public/packs/manifest.json.br: $(assets)
-e HTTP_BASIC_USER=$(HTTP_BASIC_USER) \ $(hain) 'PANEL_URL=https://panel.sutty.nl RAILS_ENV=production NODE_ENV=production bundle exec rake assets:precompile assets:clean'
-e HTTP_BASIC_PASSWORD=$(HTTP_BASIC_PASSWORD) \
-e GEM=`echo $(notdir $*) | sed -re "s/-[^-]+$$//"` \
-e VERSION=`echo $(notdir $*) | sed -re "s/.*-([^-]+)$$/\1/"` \
-e JOBS=2 \
--rm -it \
sutty/gem-compiler:latest || echo "No se pudo compilar $*"
# Compilar todas las gemas binarias y subirlas a gems.sutty.nl para que # Correr un test en particular por ejemplo
# al crear el contenedor no tengamos que compilarlas cada vez # `make test/models/usuarie_test.rb`
build-gems: $(gems_musl) tests := $(shell find test/ -name "*_test.rb")
$(tests): always
cached_gems = $(wildcard $(gem_dir)/cache/*.gem) $(MAKE) test args="TEST=$@"
rebuild_gems = $(patsubst $(gem_dir)/cache/%.gem,$(gem_dir)/$(alpine_version)/%-x86_64-linux-musl.gem,$(cached_gems))
rebuild-gems: $(rebuild_gems)
dirs := $(patsubst %,root/%,data sites deploy public)
$(dirs):
mkdir -p $@
ota: assets
sudo chgrp -R 82 public/
rsync -avi --delete-after public/ $(delegate):/srv/sutty/srv/http/data/_$(public)/
ssh $(delegate) docker exec $(container) sh -c "cat /srv/http/tmp/puma.pid | xargs -r kill -USR2"
# Hotfixes
#
# TODO: Reemplazar esto por git pull en el contenedor
commit ?= origin/rails
ota-rb:
umask 022; git format-patch $(commit)
scp ./0*.patch $(delegate):/tmp/
ssh $(delegate) mkdir -p /tmp/patches-$(commit)/
scp ./0*.patch $(delegate):/tmp/patches-$(commit)/
scp ./ota.sh $(delegate):/tmp/
ssh $(delegate) docker cp /tmp/patches-$(commit) $(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
# Agrega las direcciones locales al sistema
/etc/hosts: always /etc/hosts: always
@echo "Chequeando si es necesario agregar el dominio local $(SUTTY)" @echo "Chequeando si es necesario agregar el dominio local $(SUTTY)"
@grep -q " $(SUTTY)$$" $@ || echo -e "127.0.0.1 $(SUTTY)\n::1 $(SUTTY)" | sudo tee -a $@ @grep -q " $(SUTTY)$$" $@ || echo -e "127.0.0.1 $(SUTTY)\n::1 $(SUTTY)" | sudo tee -a $@
@ -147,4 +139,12 @@ ota-rb:
@grep -q " panel.$(SUTTY)$$" $@ || echo -e "127.0.0.1 panel.$(SUTTY)\n::1 panel.$(SUTTY)" | sudo tee -a $@ @grep -q " panel.$(SUTTY)$$" $@ || echo -e "127.0.0.1 panel.$(SUTTY)\n::1 panel.$(SUTTY)" | sudo tee -a $@
@grep -q " postgresql.$(SUTTY)$$" $@ || echo -e "127.0.0.1 postgresql.$(SUTTY)\n::1 postgresql.$(SUTTY)" | sudo tee -a $@ @grep -q " postgresql.$(SUTTY)$$" $@ || echo -e "127.0.0.1 postgresql.$(SUTTY)\n::1 postgresql.$(SUTTY)" | sudo tee -a $@
# Instala las dependencias de Javascript
node_modules: package.json
$(MAKE) yarn
# Instala las dependencias de Rails
Gemfile.lock: Gemfile
$(MAKE) bundle args=install
.PHONY: always .PHONY: always

View file

@ -15,6 +15,17 @@ Este repositorio es la plataforma _Ruby on Rails_ para alojar el
Para más información visita el [sitio de Sutty](https://sutty.nl/). Para más información visita el [sitio de Sutty](https://sutty.nl/).
### Desarrollar
Todas las tareas se gestionan con `make`, por favor instala GNU Make
antes de comenzar.
```bash
make help
```
[Leer la documentación](https://docs.sutty.nl/)
## English ## English
Sutty is a platform for hosting safer, faster and more resilient Sutty is a platform for hosting safer, faster and more resilient
@ -25,3 +36,13 @@ This repository is the Ruby on Rails platform that hosts the
self-managed [panel](https://panel.sutty.nl/). self-managed [panel](https://panel.sutty.nl/).
For more information, visit [Sutty's website](https://sutty.nl/en/). For more information, visit [Sutty's website](https://sutty.nl/en/).
### Development
Every task is run via `make`, please install GNU Make before developing.
```bash
make help
```
[Read the documentation](https://docs.sutty.nl/en/)

View file

@ -2,6 +2,13 @@
box-sizing: border-box; box-sizing: border-box;
*, *::before, *::after { box-sizing: inherit; } *, *::before, *::after { box-sizing: inherit; }
// Arreglo temporal para que las cosas sean legibles en modo oscuro
--foreground: black;
--background: white;
--color: #f206f9;
background: var(--background);
color: var(--foreground);
h1, h2, h3, h4, h5, h6, p, li { h1, h2, h3, h4, h5, h6, p, li {
min-height: 1.5rem; min-height: 1.5rem;
} }
@ -60,10 +67,18 @@
.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; }
p[data-multimedia-inner] {
// Ignorar clicks en el párrafo placeholder
pointer-events: none;
}
} }
*[data-editor-loading] { *[data-editor-loading] {

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
@ -22,7 +23,8 @@ class PostsController < ApplicationController
# XXX: Cada vez que cambiamos un Post tocamos el sitio con lo que es # XXX: Cada vez que cambiamos un Post tocamos el sitio con lo que es
# más simple saber si hubo cambios. # más simple saber si hubo cambios.
if stale?([current_usuarie, site, filter_params]) return unless stale?([current_usuarie, site, filter_params])
# Todos los artículos de este sitio para el idioma actual # Todos los artículos de este sitio para el idioma actual
@posts = site.indexed_posts.where(locale: locale) @posts = site.indexed_posts.where(locale: locale)
# De este tipo # De este tipo
@ -37,7 +39,6 @@ class PostsController < ApplicationController
# Filtrar los posts que les invitades no pueden ver # Filtrar los posts que les invitades no pueden ver
@usuarie = site.usuarie? current_usuarie @usuarie = site.usuarie? current_usuarie
end end
end
def show def show
authorize post authorize post
@ -54,7 +55,7 @@ class PostsController < ApplicationController
def new def new
authorize Post authorize Post
@post = site.posts.build(lang: locale, layout: params[:layout]) @post = site.posts(lang: locale).build(layout: params[:layout])
breadcrumb I18n.t('loaf.breadcrumbs.posts.new', layout: @post.layout.humanized_name.downcase), '' breadcrumb I18n.t('loaf.breadcrumbs.posts.new', layout: @post.layout.humanized_name.downcase), ''
end end
@ -154,7 +155,9 @@ class PostsController < ApplicationController
# #
# @return [Hash] # @return [Hash]
def filter_params def filter_params
@filter_params ||= params.permit(:q, :category, :layout).to_h.select { |_, v| v.present? } @filter_params ||= params.permit(:q, :category, :layout).to_hash.select do |_, v|
v.present?
end.transform_keys(&:to_sym)
end end
def site def site
@ -164,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

@ -3,16 +3,166 @@
# Estadísticas del sitio # Estadísticas del sitio
class StatsController < ApplicationController class StatsController < ApplicationController
include Pundit include Pundit
include ActionView::Helpers::DateHelper
before_action :authenticate_usuarie! before_action :authenticate_usuarie!
before_action :authorize_stats
EXTRA_OPTIONS = {
builds: {},
space_used: { bytes: true },
build_time: {}
}.freeze
# XXX: Permitir a Chart.js inyectar su propio CSS
content_security_policy only: :index do |policy|
policy.style_src :self, :unsafe_inline
policy.script_src :self, :unsafe_inline
end
def index def index
@chart_params = { interval: interval }
hostnames
last_stat
chart_options
normalized_urls
end
# Genera un gráfico de visitas por dominio asociado a este sitio
def host
return unless stale? [last_stat, hostnames, interval]
stats = Rollup.where_dimensions(host: hostnames).multi_series('host', interval: interval).tap do |series|
series.each do |serie|
serie[:name] = serie.dig(:dimensions, 'host')
serie[:data].transform_values! do |value|
value * nodes
end
end
end
render json: stats
end
def resources
return unless stale? [last_stat, interval, resource]
options = {
interval: interval,
dimensions: {
deploy_id: @site.deploys.where(type: 'DeployLocal').pluck(:id).first
}
}
render json: Rollup.series(resource, **options)
end
def uris
return unless stale? [last_stat, hostnames, interval, normalized_urls]
options = { host: hostnames, uri: normalized_paths }
stats = Rollup.where_dimensions(**options).multi_series('host|uri', interval: interval).tap do |series|
series.each do |serie|
serie[:name] = serie[:dimensions].slice('host', 'uri').values.join.sub('/index.html', '/')
serie[:data].transform_values! do |value|
value * nodes
end
end
end
render json: stats
end
private
def last_stat
@last_stat ||= Stat.last
end
def authorize_stats
@site = find_site @site = find_site
authorize SiteStat.new(@site) authorize SiteStat.new(@site)
end
# Solo queremos el promedio de tiempo de compilación, no de # TODO: Eliminar cuando mergeemos referer-origin
# instalación de dependencias. def hostnames
stats = @site.build_stats.jekyll @hostnames ||= [@site.hostname, @site.alternative_hostnames].flatten
@build_avg = stats.average(:seconds).to_f.round(2) end
@build_max = stats.maximum(:seconds).to_f.round(2)
# Normalizar las URLs
#
# @return [Array]
def normalized_urls
@normalized_urls ||= params.permit(:urls).try(:[],
:urls)&.split("\n")&.map(&:strip)&.select(&:present?)&.select do |uri|
uri.start_with? 'https://'
end&.map do |u|
# XXX: Eliminar
# @see {https://0xacab.org/sutty/containers/nginx/-/merge_requests/1}
next u unless u.end_with? '/'
"#{u}index.html"
end&.uniq || [@site.url, @site.urls].flatten.uniq
end
def normalized_paths
@normalized_paths ||= normalized_urls.map do |u|
"/#{u.split('/', 4).last}"
end.map do |u|
URI.decode_www_form_component u
end
end
# Opciones por defecto para los gráficos.
#
# La invitación a volver dentro de X tiempo es para dar un estimado de
# cuándo habrá información disponible, porque Rollup genera intervalos
# completos (¿aunque dice que no?)
#
# La diferencia se calcula sumando el intervalo a la hora de última
# toma de estadísticas y restando el tiempo que pasó desde ese
# momento.
def chart_options
time = (last_stat&.created_at || Time.now) + 1.try(interval)
please_return_at = { please_return_at: distance_of_time_in_words(Time.now, time) }
@chart_options ||= {
locale: I18n.locale,
empty: I18n.t('stats.index.empty', **please_return_at),
loading: I18n.t('stats.index.loading'),
html: %(<div id="%{id}" class="d-flex align-items-center justify-content-center" style="height: %{height}; width: %{width};">%{loading}</div>)
}
end
# Obtiene y valida los intervalos
#
# @return [Symbol]
def interval
@interval ||= begin
i = params[:interval]&.to_sym
Stat::INTERVALS.include?(i) ? i : :day
end
end
def resource
@resource ||= begin
r = params[:resource].to_sym
Stat::RESOURCES.include?(r) ? r : :builds
end
end
# Obtiene la cantidad de nodos de Sutty, para poder calcular la
# cantidad de visitas.
#
# Como repartimos las visitas por nodo rotando las IPs en el
# nameserver y los resolvedores de DNS eligen un nameserver
# aleatoriamente, la cantidad de visitas se reparte
# equitativamente.
#
# XXX: Remover cuando podamos centralizar los AccessLog
#
# @return [Integer]
def nodes
@nodes ||= ENV.fetch('NODES', 1).to_i
end 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

@ -137,8 +137,10 @@ export function setupAuxiliaryToolbar(editor: Editor): void {
"click", "click",
(event) => { (event) => {
const files = editor.toolbar.auxiliary.multimedia.fileEl.files; const files = editor.toolbar.auxiliary.multimedia.fileEl.files;
if (!files || !files.length) if (!files || !files.length) {
throw new Error("no hay archivos para subir"); console.info("no hay archivos para subir");
return;
}
const file = files[0]; const file = files[0];
const selectedEl = editor.contentEl.querySelector<HTMLElement>( const selectedEl = editor.contentEl.querySelector<HTMLElement>(

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

@ -18,6 +18,7 @@ import 'etc'
import Rails from '@rails/ujs' import Rails from '@rails/ujs'
import Turbolinks from 'turbolinks' import Turbolinks from 'turbolinks'
import * as ActiveStorage from '@rails/activestorage' import * as ActiveStorage from '@rails/activestorage'
import 'chartkick/chart.js'
Rails.start() Rails.start()
Turbolinks.start() Turbolinks.start()

View file

@ -5,13 +5,23 @@ class DeployJob < ApplicationJob
class DeployException < StandardError; end class DeployException < StandardError; end
# rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/MethodLength
def perform(site, notify = true) def perform(site, notify = true, time = Time.now)
ActiveRecord::Base.connection_pool.with_connection do ActiveRecord::Base.connection_pool.with_connection do
@site = Site.find(site) @site = Site.find(site)
# Si ya hay una tarea corriendo, aplazar esta # Si ya hay una tarea corriendo, aplazar esta. Si estuvo
# esperando más de 10 minutos, recuperar el estado anterior.
#
# Como el trabajo actual se aplaza al siguiente, arrastrar la
# hora original para poder ir haciendo timeouts.
if @site.building? if @site.building?
DeployJob.perform_in(60, site, notify) if 10.minutes.ago >= time
@site.update status: 'waiting'
raise DeployException,
"#{@site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
end
DeployJob.perform_in(60, site, notify, time)
return return
end end
@ -29,8 +39,11 @@ class DeployJob < ApplicationJob
end end
deploy_others deploy_others
notify_usuaries if notify
# Volver a la espera
@site.update status: 'waiting' @site.update status: 'waiting'
notify_usuaries if notify
end end
end end
# rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/MethodLength

55
app/jobs/periodic_job.rb Normal file
View file

@ -0,0 +1,55 @@
# frozen_string_literal: true
# Una tarea que se corre periódicamente
class PeriodicJob < ApplicationJob
class RunAgainException < StandardError; end
STARTING_INTERVAL = Stat::INTERVALS.first
# Tener el sitio a mano
attr_reader :site
# Descartar y notificar si pasó algo más.
#
# XXX: En realidad deberíamos seguir reintentando?
discard_on(StandardError) do |_, error|
ExceptionNotifier.notify_exception(error)
end
# Correr indefinidamente una vez por hora.
#
# XXX: El orden importa, si el descarte viene después, nunca se va a
# reintentar.
retry_on(PeriodicJob::RunAgainException, wait: 1.try(STARTING_INTERVAL), attempts: Float::INFINITY, jitter: 0)
private
# Las clases que implementen esta tienen que usar este método al
# terminar.
def run_again!
raise PeriodicJob::RunAgainException, 'Reintentando'
end
# El intervalo de inicio
#
# @return [Symbol]
def starting_interval
STARTING_INTERVAL
end
# La última recolección de estadísticas o empezar desde el principio
# de los tiempos.
#
# @return [Stat]
def last_stat
@last_stat ||= site.stats.where(name: stat_name).last ||
site.stats.build(created_at: Time.new(1970, 1, 1))
end
# Devuelve el comienzo del intervalo
#
# @return [Time]
def beginning_of_interval
@beginning_of_interval ||= last_stat.created_at.try(:"beginning_of_#{starting_interval}")
end
end

View file

@ -0,0 +1,67 @@
# frozen_string_literal: true
# Genera resúmenes de información para poder mostrar estadísticas y se
# corre regularmente a sí misma.
class StatCollectionJob < ApplicationJob
STAT_NAME = 'stat_collection_job'
def perform(site_id:, once: true)
@site = Site.find site_id
scope.rollup('builds', **options)
scope.rollup('space_used', **options) do |rollup|
rollup.average(:bytes)
end
scope.rollup('build_time', **options) do |rollup|
rollup.average(:seconds)
end
# XXX: Es correcto promediar promedios?
Stat::INTERVALS.reduce do |previous, current|
rollup(name: 'builds', interval_previous: previous, interval: current)
rollup(name: 'space_used', interval_previous: previous, interval: current, operation: :average)
rollup(name: 'build_time', interval_previous: previous, interval: current, operation: :average)
current
end
# Registrar que se hicieron todas las recolecciones
site.stats.create! name: STAT_NAME
run_again! unless once
end
private
# Genera un rollup recursivo en base al período anterior y aplica una
# operación.
#
# @return [NilClass]
def rollup(name:, interval_previous:, interval:, operation: :sum)
Rollup.where(name: name, interval: interval_previous)
.where_dimensions(site_id: site.id)
.group("dimensions->'site_id'")
.rollup(name, interval: interval, update: true) do |rollup|
rollup.try(:operation, :value)
end
end
# Los registros a procesar
#
# @return [ActiveRecord::Relation]
def scope
@scope ||= site.build_stats
.jekyll
.where('created_at => ?', beginning_of_interval)
.group(:site_id)
end
# Las opciones por defecto
#
# @return [Hash]
def options
@options ||= { interval: starting_interval, update: true }
end
end

View file

@ -0,0 +1,106 @@
# frozen_string_literal: true
# Procesar una lista de URIs para una lista de dominios. Esto nos
# permite procesar estadísticas a demanda.
#
# Hay varias cosas acá que van a convertirse en métodos propios, como la
# detección de URIs de un sitio (aunque la versión actual detecta todas
# las páginas y no solo las de posts como tenemos planeado, hay que
# resolver eso).
#
# Los hostnames de un sitio van a poder obtenerse a partir de
# Site#hostnames con la garantía de que son únicos.
class UriCollectionJob < PeriodicJob
# Ignoramos imágenes porque suelen ser demasiadas y no aportan a las
# estadísticas.
IMAGES = %w[.png .jpg .jpeg .gif .webp].freeze
STAT_NAME = 'uri_collection_job'
def perform(site_id:, once: true)
@site = Site.find site_id
hostnames.each do |hostname|
uris.each do |uri|
return if File.exist? Rails.root.join('tmp', 'uri_collection_job_stop')
AccessLog.where(host: hostname, uri: uri)
.where('created_at >= ?', beginning_of_interval)
.completed_requests
.non_robots
.group(:host, :uri)
.rollup('host|uri', interval: starting_interval, update: true)
# Reducir las estadísticas calculadas aplicando un rollup sobre el
# intervalo más amplio.
Stat::INTERVALS.reduce do |previous, current|
Rollup.where(name: 'host|uri', interval: previous)
.where_dimensions(host: hostname, uri: uri)
.group("dimensions->'host'", "dimensions->'uri'")
.rollup('host|uri', interval: current, update: true) do |rollup|
rollup.sum(:value)
end
# Devolver el intervalo actual
current
end
end
end
# Recordar la última vez que se corrió la tarea
site.stats.create! name: STAT_NAME
run_again! unless once
end
private
def stat_name
STAT_NAME
end
# @return [String]
#
# TODO: Cambiar al mergear origin-referer
def destination
@destination ||= site.deploys.find_by(type: 'DeployLocal').destination
end
# TODO: Cambiar al mergear origin-referer
#
# @return [Array]
def hostnames
@hostnames ||= site.deploys.map do |deploy|
case deploy
when DeployLocal
site.hostname
when DeployWww
deploy.fqdn
when DeployAlternativeDomain
deploy.hostname.dup.tap do |h|
h.replace(h.end_with?('.') ? h[0..-2] : "#{h}.#{Site.domain}")
end
when DeployHiddenService
deploy.onion
end
end.compact
end
# Recolecta todas las URIs menos imágenes
#
# @return [Array]
def uris
@uris ||= Dir.chdir destination do
(Dir.glob('**/*.html') + Dir.glob('public/**/*').reject do |p|
File.directory? p
end.reject do |p|
p = p.downcase
IMAGES.any? do |i|
p.end_with? i
end
end).map do |uri|
"/#{uri}"
end
end
end
end

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

@ -1,4 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class AccessLog < ApplicationRecord class AccessLog < ApplicationRecord
# Las peticiones completas son las que terminaron bien y se
# respondieron con 200 OK o 304 Not Modified
#
# @see {https://en.wikipedia.org/wiki/List_of_HTTP_status_codes}
scope :completed_requests, -> { where(request_method: 'GET', request_completion: 'OK', status: [200, 304]) }
scope :non_robots, -> { where(crawler: false) }
scope :robots, -> { where(crawler: true) }
scope :pages, -> { where(sent_http_content_type: ['text/html', 'text/html; charset=utf-8', 'text/html; charset=UTF-8']) }
end end

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

@ -62,10 +62,15 @@ class DeployLocal < Deploy
'AIRBRAKE_PROJECT_ID' => site.id.to_s, 'AIRBRAKE_PROJECT_ID' => site.id.to_s,
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key, 'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
'JEKYLL_ENV' => Rails.env, 'JEKYLL_ENV' => Rails.env,
'LANG' => ENV['LANG'] 'LANG' => ENV['LANG'],
'YARN_CACHE_FOLDER' => yarn_cache_dir
} }
end end
def yarn_cache_dir
Rails.root.join('_yarn_cache').to_s
end
def yarn_lock def yarn_lock
File.join(site.path, 'yarn.lock') File.join(site.path, 'yarn.lock')
end end
@ -80,9 +85,9 @@ class DeployLocal < Deploy
# Corre yarn dentro del repositorio # Corre yarn dentro del repositorio
def yarn def yarn
return unless yarn_lock? return true unless yarn_lock?
run 'yarn' run 'yarn install --production'
end end
def bundle def bundle

View file

@ -13,6 +13,13 @@ class MetadataBelongsTo < MetadataRelatedPosts
'' ''
end end
# Obtiene el valor desde el documento.
#
# @return [String]
def document_value
document.data[name.to_s]
end
def validate def validate
super super

View file

@ -25,10 +25,19 @@ class MetadataBoolean < MetadataTemplate
# * false # * false
# * true # * true
def value def value
return document.data.fetch(name.to_s, default_value) if self[:value].nil? case self[:value]
return self[:value] unless self[:value].is_a? String when NilClass
document.data.fetch(name.to_s, default_value)
when String
true_values.include? self[:value]
else
self[:value]
end
end
self[:value] = true_values.include? self[:value] # Siempre guardar el valor de este campo a menos que sea nulo
def empty?
value.nil?
end end
private private

View file

@ -56,7 +56,7 @@ class MetadataContent < MetadataTemplate
uri = URI element['src'] uri = URI element['src']
# No permitimos recursos externos # No permitimos recursos externos
element.remove unless uri.hostname.end_with? Site.domain element.remove unless uri.scheme == 'https' && uri.hostname.end_with?(Site.domain)
rescue URI::Error rescue URI::Error
element.remove element.remove
end end

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

@ -0,0 +1,31 @@
# frozen_string_literal: true
# Un campo numérico de punto flotante
class MetadataFloat < MetadataTemplate
# Nada
def default_value
super || nil
end
def save
return true unless changed?
self[:value] = value.to_f
self[:value] = encrypt(value) if private?
true
end
# Indicarle al navegador que acepte números decimales
#
# @return [Float]
def step
0.05
end
private
def decrypt(value)
super(value).to_f
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

@ -2,6 +2,12 @@
# Este metadato permite generar rutas manuales. # Este metadato permite generar rutas manuales.
class MetadataPermalink < MetadataString class MetadataPermalink < MetadataString
# El valor por defecto una vez creado es la URL que le asigne Jekyll,
# de forma que nunca cambia aunque se cambie el título.
def default_value
document.url.sub(%r{\A/}, '') unless post.new?
end
# Los permalinks nunca pueden ser privados # Los permalinks nunca pueden ser privados
def private? def private?
false false
@ -13,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

@ -0,0 +1,24 @@
# frozen_string_literal: true
# Un campo de texto seleccionado de una lista de valores posibles
class MetadataPredefinedValue < MetadataString
# Obtiene todos los valores desde el layout, en un formato compatible
# con options_for_select.
#
# @return [Hash]
def values
@values ||= layout.dig(:metadata, name, 'values', I18n.locale.to_s)&.invert || {}
end
private
# Solo permite almacenar los valores predefinidos.
#
# @return [String]
def sanitize(string)
v = super string
return '' unless values.values.include? v
v
end
end

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
.sanitize(string.tr("\r", '').unicode_normalize,
tags: allowed_tags, tags: allowed_tags,
attributes: allowed_attributes).strip.html_safe 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

@ -1,360 +0,0 @@
# frozen_string_literal: true
# Representa los distintos tipos de campos que pueden venir de una
# plantilla compleja
class Post
class TemplateField
attr_reader :post, :contents, :key
STRING_VALUES = %w[string text url number email password date year
image video audio document].freeze
# Tipo de valores que son archivos
FILE_TYPES = %w[image video audio document].freeze
def initialize(post, key, contents)
@post = post
@key = key
@contents = contents
end
def title
contents.dig('title') if complex?
end
def subtitle
contents.dig('subtitle') if complex?
end
# Obtiene el valor
def value
complex? ? contents.dig('value') : contents
end
def max
return 0 if simple?
contents.fetch('max', 0)
end
def min
return 0 if simple?
contents.fetch('min', 0)
end
# TODO: volver elegante!
def type
return @type if @type
if image?
@type = 'image'
elsif email?
@type = 'email'
elsif url?
@type = 'url'
elsif number?
@type = 'number'
elsif password?
@type = 'password'
elsif date?
@type = 'date'
elsif year?
@type = 'year'
elsif text_area?
@type = 'text_area'
elsif check_box_group?
@type = 'check_box_group'
elsif radio_group?
@type = 'radio_group'
elsif string?
@type = 'text'
# TODO: volver a hacer funcionar esto y ahorranos los multiple:
# false
elsif string? && contents.split('/', 2).count == 2
@type = 'select'
elsif nested?
@type = 'table'
elsif array?
@type = 'select'
elsif boolean?
@type = 'check_box'
end
@type
end
# Devuelve los valores vacíos según el tipo
def empty_value
if string?
''
elsif nested?
# TODO: devolver las keys también
{}
elsif array?
[]
elsif boolean?
false
end
end
def cols
complex? && contents.dig('cols')
end
def align
complex? && contents.dig('align')
end
# El campo es requerido si es complejo y se especifica que lo sea
def required?
complex? && contents.dig('required')
end
def boolean?
value.is_a?(FalseClass) || value.is_a?(TrueClass)
end
def string?
value.is_a? String
end
def text_area?
value == 'text'
end
def url?
value == 'url'
end
def email?
value == 'email' || value == 'mail'
end
alias mail? email?
def date?
value == 'date'
end
def password?
value == 'password'
end
def number?
value == 'number'
end
def year?
value == 'year'
end
def file?
string? && FILE_TYPES.include?(value)
end
def image?
array? ? value.first == 'image' : value == 'image'
end
# Si la plantilla es simple no está admitiendo Hashes como valores
def simple?
!complex?
end
def complex?
contents.is_a? Hash
end
# XXX Retrocompatibilidad
def to_s
key
end
# Convierte el campo en un parámetro
def to_param
if nested?
{ key.to_sym => {} }
elsif array? && multiple?
{ key.to_sym => [] }
else
key.to_sym
end
end
# Convierte la plantilla en el formato de front_matter
def to_front_matter
{ key => empty_value }
end
def check_box_group?
array? && (complex? && contents.fetch('checkbox', false))
end
def radio_group?
array? && (complex? && contents.fetch('radio', false))
end
def array?
value.is_a? Array
end
# TODO: detectar cuando es complejo y tomar el valor de :multiple
def multiple?
# si la plantilla es simple, es multiple cuando tenemos un array
return array? if simple?
array? && contents.fetch('multiple', true)
end
# Detecta si el valor es una tabla de campos
def nested?
value.is_a?(Hash) || (array? && value.first.is_a?(Hash))
end
# Un campo acepta valores abiertos si no es un array con múltiples
# elementos
def open?
# Todos los valores simples son abiertos
return true unless complex?
return false unless array?
# La cosa se complejiza cuando tenemos valores complejos
#
# Si tenemos una lista cerrada de valores, necesitamos saber si el
# campo es abierto o cerrado. Si la lista tiene varios elementos,
# es una lista cerrada, opcionalmente abierta. Si la lista tiene
# un elemento, quiere decir que estamos autocompletando desde otro
# lado.
contents.fetch('open', value.count < 2)
end
def closed?
!open?
end
# Determina si los valores del campo serán públicos después
#
# XXX Esto es solo una indicación, el theme Jekyll tiene que
# respetarlos por su lado luego
def public?
# Todos los campos son públicos a menos que se indique lo
# contrario
simple? || contents.fetch('public', true)
end
def private?
!public?
end
def human
h = key.humanize
h
end
def label
h = (complex? && contents.dig('label')) || human
h += ' *' if required?
h
end
def help
complex? && contents.dig('help')
end
def nested_fields
return unless nested?
v = value
v = value.first if array?
@nested_fields ||= v.map do |k, sv|
Post::TemplateField.new post, k, sv
end
end
# Obtiene los valores posibles para el campo de la plantilla
def values
return 'false' if value == false
return 'true' if value == true
# XXX por alguna razón `value` no refiere a value() :/
return '' if STRING_VALUES.include? value
# Las listas cerradas no necesitan mayor procesamiento
return value if array? && closed? && value.count > 1
# Y las vacías tampoco
return value if array? && value.empty?
# Ahorrarnos el trabajo
return @values if @values
# Duplicar el valor para no tener efectos secundarios luego (?)
value = self.value.dup
# Para obtener los valores posibles, hay que procesar la string y
# convertirla a parametros
# Si es una array de un solo elemento, es un indicador de que
# tenemos que rellenarla con los valores que indica.
#
# El primer valor es el que trae la string de autocompletado
values = array? ? value.shift : value
# Si el valor es un array con más de un elemento, queremos usar
# esas opciones. Pero si además es abierto, queremos traer los
# valores cargados anteriormente.
# Procesamos el valor, buscando : como separador de campos que
# queremos encontrar y luego los unimos
_value = (values&.split(':', 2) || []).map do |v|
# Tenemos hasta tres niveles de búsqueda
collection, attr, subattr = v.split('/', 3)
if collection == 'site'
# TODO: puede ser peligroso permitir acceder a cualquier
# atributo de site? No estamos trayendo nada fuera de
# lo normal
post.site.send(attr.to_sym)
# Si hay un subatributo, tenemos que averiguar todos los
# valores dentro de el
# TODO volver elegante!
# TODO volver recursivo!
elsif subattr
post.site.everything_of(attr, lang: collection)
.compact
.map { |sv| sv[subattr] }
.flatten
.compact
.uniq
else
post.site.everything_of(attr, lang: collection).compact
end
end
# Si el valor es abierto, sumar los valores auto-completados a
# lo pre-cargados.
#
# En este punto _value es un array de 1 o 2 arrays, si es de uno,
# value tambien tiene que serlo. Si es de 2, hay que unir cada
# una
if open?
if _value.count == 1
_value = [(_value.first + value).uniq]
elsif _value.count == 2
_value = _value.each_with_index.map do |v, i|
v + value.fetch(i, [])
end
end
end
# Crea un array de arrays, útil para los select
# [ [ 1, a ], [ 2, b ] ]
# aunque si no hay un : en el autocompletado, el array queda
# [ [ 1, 1 ], [ 2, 2 ] ]
values = _value.empty? ? [] : _value.last.zip(_value.first)
# En última instancia, traer el valor por defecto y ahorrarnos
# volver a procesar
@values = values
end
end
end

View file

@ -26,7 +26,7 @@ class Site < ApplicationRecord
validates :design_id, presence: true validates :design_id, presence: true
validates_inclusion_of :status, in: %w[waiting enqueued building] validates_inclusion_of :status, in: %w[waiting enqueued building]
validates_presence_of :title validates_presence_of :title
validates :description, length: { in: 50..160 } validates :description, length: { in: 10..160 }
validate :deploy_local_presence validate :deploy_local_presence
validate :compatible_layouts, on: :update validate :compatible_layouts, on: :update
@ -37,6 +37,7 @@ class Site < ApplicationRecord
belongs_to :design belongs_to :design
belongs_to :licencia belongs_to :licencia
has_many :stats
has_many :log_entries, dependent: :destroy has_many :log_entries, dependent: :destroy
has_many :deploys, dependent: :destroy has_many :deploys, dependent: :destroy
has_many :build_stats, through: :deploys has_many :build_stats, through: :deploys
@ -56,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!
@ -65,9 +66,6 @@ class Site < ApplicationRecord
accepts_nested_attributes_for :deploys, allow_destroy: true accepts_nested_attributes_for :deploys, allow_destroy: true
# El sitio en Jekyll
attr_reader :jekyll
# XXX: Es importante incluir luego de los callbacks de :load_jekyll # XXX: Es importante incluir luego de los callbacks de :load_jekyll
include Site::Index include Site::Index
@ -180,29 +178,28 @@ class Site < ApplicationRecord
# Trae los datos del directorio _data dentro del sitio # Trae los datos del directorio _data dentro del sitio
def data def data
unless @jekyll.data.present? unless jekyll.data.present?
@jekyll.reader.read_data run_in_path do
jekyll.reader.read_data
# Define los valores por defecto según la llave buscada jekyll.data['layouts'] ||= {}
@jekyll.data.default_proc = proc do |data, key|
data[key] = case key
when 'layout' then {}
end
end end
end end
@jekyll.data jekyll.data
end end
# Traer las colecciones. Todos los artículos van a estar dentro de # Traer las colecciones. Todos los artículos van a estar dentro de
# colecciones. # colecciones.
def collections def collections
unless @read unless @read
@jekyll.reader.read_collections run_in_path do
jekyll.reader.read_collections
end
@read = true @read = true
end end
@jekyll.collections jekyll.collections
end end
# Traer la configuración de forma modificable # Traer la configuración de forma modificable
@ -290,7 +287,9 @@ class Site < ApplicationRecord
# #
# @return [Hash] # @return [Hash]
def theme_layouts def theme_layouts
@jekyll.reader.read_layouts run_in_path do
jekyll.reader.read_layouts
end
end end
# Trae todos los valores disponibles para un campo # Trae todos los valores disponibles para un campo
@ -332,6 +331,12 @@ class Site < ApplicationRecord
status == 'building' status == 'building'
end end
def jekyll
run_in_path do
@jekyll ||= Jekyll::Site.new(configuration)
end
end
# Cargar el sitio Jekyll # Cargar el sitio Jekyll
# #
# TODO: En lugar de leer todo junto de una vez, extraer la carga de # TODO: En lugar de leer todo junto de una vez, extraer la carga de
@ -345,10 +350,7 @@ class Site < ApplicationRecord
def reload_jekyll! def reload_jekyll!
reset reset
jekyll
Dir.chdir(path) do
@jekyll = Jekyll::Site.new(configuration)
end
end end
def reload def reload
@ -472,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
# #
@ -526,4 +523,8 @@ class Site < ApplicationRecord
errors.add(:design_id, errors.add(:design_id,
I18n.t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.error')) I18n.t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.error'))
end end
def run_in_path(&block)
Dir.chdir path, &block
end
end end

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

10
app/models/stat.rb Normal file
View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
# Registran cuándo fue la última recolección de datos.
class Stat < ApplicationRecord
# XXX: Los intervalos van en orden de mayor especificidad a menor
INTERVALS = %i[day month year].freeze
RESOURCES = %i[builds space_used build_time].freeze
belongs_to :site
end

View file

@ -12,4 +12,16 @@ class SiteStatPolicy
def index? def index?
site_stat.site.usuarie? usuarie site_stat.site.usuarie? usuarie
end end
def host?
index?
end
def resources?
index?
end
def uris?
index?
end
end end

View file

@ -122,6 +122,8 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
# la búsqueda. # la búsqueda.
def change_licencias def change_licencias
site.locales.each do |locale| site.locales.each do |locale|
next unless I18n.available_locales.include? locale
Mobility.with_locale(locale) do Mobility.with_locale(locale) do
permalink = "#{I18n.t('activerecord.models.licencia').downcase}/" permalink = "#{I18n.t('activerecord.models.licencia').downcase}/"
post = site.posts(lang: locale).find_by(permalink: permalink) post = site.posts(lang: locale).find_by(permalink: permalink)

View file

@ -0,0 +1,3 @@
%tr{ id: attribute }
%th= post_label_t(attribute, post: post)
%td{ dir: dir, lang: locale }= metadata.value

View file

@ -0,0 +1,3 @@
%tr{ id: attribute }
%th= post_label_t(attribute, post: post)
%td{ dir: dir, lang: locale }= metadata.value

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

@ -0,0 +1,6 @@
.form-group
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
= number_field base, attribute, value: metadata.value, step: metadata.step,
**field_options(attribute, metadata)
= render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata

View file

@ -0,0 +1,7 @@
.form-group
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
= select_tag(plain_field_name_for(base, attribute),
options_for_select(metadata.values, metadata.value),
**field_options(attribute, metadata), include_blank: t('.empty'))
= render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata

View file

@ -40,7 +40,7 @@
%section.col %section.col
= render 'layouts/flash' = render 'layouts/flash'
.d-flex.justify-content-between.align-items-center.pl-2-plus.pr-2-plus.mb-2 .d-flex.justify-content-between.align-items-center.pl-2-plus.pr-2-plus.mb-2
%form %form{ action: site_posts_path }
- @filter_params.each do |param, value| - @filter_params.each do |param, value|
- next if param == 'q' - next if param == 'q'
%input{ type: 'hidden', name: param, value: value } %input{ type: 'hidden', name: param, value: value }
@ -51,7 +51,7 @@
- if @site.locales.size > 1 - if @site.locales.size > 1
%nav#locales %nav#locales
- @site.locales.each do |locale| - @site.locales.each do |locale|
= link_to t("locales.#{locale}.name"), site_posts_path(@site, **@filter_params.merge(locale: locale)), = link_to @site.data.dig(locale.to_s, 'locale') || locale, site_posts_path(@site, **@filter_params.merge(locale: locale)),
class: "mr-2 mt-2 mb-2 #{locale == @locale ? 'active font-weight-bold' : ''}" class: "mr-2 mt-2 mb-2 #{locale == @locale ? 'active font-weight-bold' : ''}"
.pl-2-plus .pl-2-plus
- @filter_params.each do |param, value| - @filter_params.each do |param, value|
@ -67,11 +67,14 @@
%h2= t('posts.empty') %h2= t('posts.empty')
- else - else
= form_tag site_posts_reorder_path, method: :post do = form_tag site_posts_reorder_path, method: :post do
%input{ type: 'hidden', name: 'post[lang]', value: @locale }
%table.table{ data: { controller: 'reorder' } } %table.table{ data: { controller: 'reorder' } }
%caption.sr-only= t('posts.caption') %caption.sr-only= t('posts.caption')
%thead %thead
%tr %tr
%th.border-0.background-white.position-sticky{ style: 'top: 0; z-index: 2', colspan: '4' } %th.border-0.background-white.position-sticky{ style: 'top: 0; z-index: 2', colspan: '4' }
.d-flex.flex-row.justify-content-between
%div
= submit_tag t('posts.reorder.submit'), class: 'btn' = submit_tag t('posts.reorder.submit'), class: 'btn'
%button.btn{ data: { action: 'reorder#unselect' } } %button.btn{ data: { action: 'reorder#unselect' } }
= t('posts.reorder.unselect') = t('posts.reorder.unselect')
@ -80,6 +83,8 @@
%button.btn{ data: { action: 'reorder#down' } }= t('posts.reorder.down') %button.btn{ data: { action: 'reorder#down' } }= t('posts.reorder.down')
%button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top') %button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top')
%button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom') %button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
%div
%tbody %tbody
- dir = t("locales.#{@locale}.dir") - dir = t("locales.#{@locale}.dir")
- size = @posts.size - size = @posts.size
@ -104,19 +109,19 @@
%span{ lang: post.locale, dir: dir }= post.title %span{ lang: post.locale, dir: dir }= post.title
- if post.front_matter['draft'].present? - if post.front_matter['draft'].present?
%span.badge.badge-primary= I18n.t('posts.attributes.draft.label') %span.badge.badge-primary= I18n.t('posts.attributes.draft.label')
- if post.front_matter['categories'].present?
%br %br
%small %small
- post.front_matter['categories'].each do |category| = link_to @site.layouts[post.layout].humanized_name, site_posts_path(@site, **@filter_params.merge(layout: post.layout))
- post.front_matter['categories']&.each do |category|
= link_to site_posts_path(@site, **@filter_params.merge(category: category)) do = link_to site_posts_path(@site, **@filter_params.merge(category: category)) do
%span{ lang: post.locale, dir: dir }= category %span{ lang: post.locale, dir: dir }= category
= '/' unless post.front_matter['categories'].last == category = '/' unless post.front_matter['categories'].last == category
%td %td.text-nowrap
= post.created_at.strftime('%F') = post.created_at.strftime('%F')
%br/ %br/
= post.order = post.order
%td %td.text-nowrap
- if @usuarie || policy(post).edit? - if @usuarie || policy(post).edit?
= link_to t('posts.edit'), edit_site_post_path(@site, post.path), class: 'btn btn-block' = link_to t('posts.edit'), edit_site_post_path(@site, post.path), class: 'btn btn-block'
- if @usuarie || policy(post).destroy? - if @usuarie || policy(post).destroy?

View file

@ -38,4 +38,4 @@
- cache [metadata, I18n.locale] do - cache [metadata, I18n.locale] do
%section.editor{ id: attr, dir: dir } %section.editor{ id: attr, dir: dir }
= @post.public_send(attr).to_s.html_safe = @post.public_send(attr).value.html_safe

View file

@ -39,7 +39,7 @@
%h2= f.label :description %h2= f.label :description
%p.lead= t('.help.description') %p.lead= t('.help.description')
= f.text_area :description, class: form_control(site, :description), = f.text_area :description, class: form_control(site, :description),
maxlength: 160, minlength: 50, required: true maxlength: 160, minlength: 10, required: true
- if invalid? site, :description - if invalid? site, :description
.invalid-feedback= site.errors.messages[:description].join(', ') .invalid-feedback= site.errors.messages[:description].join(', ')
%hr/ %hr/

View file

@ -1,17 +1,43 @@
= render 'layouts/breadcrumb',
crumbs: [link_to(t('sites.index.title'), sites_path),
link_to(@site.name, site_path(@site)), t('.title')]
.row .row
.col .col
%h1= t('.title') %h1= t('.title')
%p.lead= t('.help') %p.lead= t('.help')
- if @last_stat
%p
%small
= t('.last_update')
%time{ datetime: @last_stat.created_at }
#{time_ago_in_words @last_stat.created_at}.
%table.table.table-condensed .mb-5
%tbody - Stat::INTERVALS.each do |interval|
%tr = link_to t(".#{interval}"), site_stats_path(interval: interval, urls: params[:urls]), class: "btn #{'btn-primary active' if @interval == interval}"
%td= t('.build.average')
%td= distance_of_time_in_words_if_more_than_a_minute @build_avg .mb-5
%tr %h2= t('.host.title', count: @hostnames.size)
%td= t('.build.maximum') %p.lead= t('.host.description')
%td= distance_of_time_in_words_if_more_than_a_minute @build_max = line_chart site_stats_host_path(@chart_params), **@chart_options
.mb-5
%h2= t('.urls.title')
%p.lead= t('.urls.description')
%form
%input{ type: 'hidden', name: 'interval', value: @interval }
.form-group
%label{ for: 'urls' }= t('.urls.label')
%textarea#urls.form-control{ name: 'urls', autocomplete: 'on', required: true, rows: @normalized_urls.size, aria_describedby: 'help-urls' }= @normalized_urls.join("\n")
%small#help-urls.feedback.form-text.text-muted= t('.urls.help')
.form-group
%button.btn{ type: 'submit' }= t('.urls.submit')
- if @normalized_urls.present?
= line_chart site_stats_uris_path(urls: params[:urls], **@chart_params), **@chart_options
.mb-5
%h2= t('.resources.title')
%p.lead= t('.resources.description')
- Stat::RESOURCES.each do |resource|
.mb-5
%h3= t(".resources.#{resource}.title")
%p.lead= t(".resources.#{resource}.description")
= line_chart site_stats_resources_path(resource: resource, **@chart_params), **@chart_options.merge(StatsController::EXTRA_OPTIONS[resource])

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

@ -11,6 +11,8 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.singular 'licencias', 'licencia' inflect.singular 'licencias', 'licencia'
inflect.plural 'rol', 'roles' inflect.plural 'rol', 'roles'
inflect.singular 'roles', 'rol' inflect.singular 'roles', 'rol'
inflect.plural 'rollup', 'rollups'
inflect.singular 'rollups', 'rollup'
end end
ActiveSupport::Inflector.inflections(:es) do |inflect| ActiveSupport::Inflector.inflections(:es) do |inflect|
@ -24,4 +26,6 @@ ActiveSupport::Inflector.inflections(:es) do |inflect|
inflect.singular 'roles', 'rol' inflect.singular 'roles', 'rol'
inflect.plural 'licencia', 'licencias' inflect.plural 'licencia', 'licencias'
inflect.singular 'licencias', 'licencia' inflect.singular 'licencias', 'licencia'
inflect.plural 'rollup', 'rollups'
inflect.singular 'rollups', 'rollup'
end end

View file

@ -19,8 +19,8 @@ en:
remember_me: 'Keeps session open for %{remember_for}' remember_me: 'Keeps session open for %{remember_for}'
actions: actions:
sr-help: "After this form you'll find links to recover your account and other actions." sr-help: "After this form you'll find links to recover your account and other actions."
_true: Yes _true: 'Yes'
_false: No _false: 'No'
svg: svg:
sutty: sutty:
title: Sutty title: Sutty
@ -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'
@ -163,7 +165,7 @@ en:
signature: 'With love, Sutty' signature: 'With love, Sutty'
breadcrumb: breadcrumb:
title: 'Your location in Sutty' title: 'Your location in Sutty'
logout: Exit logout: Log out
mutual_aid: Mutual aid mutual_aid: Mutual aid
collaborations: collaborations:
collaborate: collaborate:
@ -252,9 +254,38 @@ en:
help: | help: |
These statistics show information about how your site is generated and These statistics show information about how your site is generated and
how many resources it uses. how many resources it uses.
build: last_update: 'Updated every hour. Last update on '
average: 'Average building time' empty: 'There is no enough information yet. We invite you to come back in %{please_return_at}!'
maximum: 'Maximum building time' loading: 'Loading...'
hour: 'Hourly'
day: 'Daily'
week: 'Weekly'
month: 'Monthly'
year: 'Yearly'
host:
title:
zero: 'Site visits'
one: 'Site visits'
other: 'Visits by domain name'
description: 'Counts visited pages on your site, grouped by domain names in use.'
urls:
title: 'Visits by URL'
description: 'Counts visits or downloads on any URL.'
label: 'URLs ("links")'
help: 'Copy and paste a single URL per line'
submit: 'Update graph'
resources:
title: 'Resource usage'
description: "In this section you can find statistics on your site's use of Sutty's shared resources"
builds:
title: 'Site publication'
description: 'Times you published your site.'
space_used:
title: 'Server disk usage'
description: 'Average storage space used by your site.'
build_time:
title: 'Publication time'
description: 'Average time your site takes to build.'
sites: sites:
donations: donations:
url: 'https://donaciones.sutty.nl/en/' url: 'https://donaciones.sutty.nl/en/'
@ -376,6 +407,8 @@ en:
en: 'English' en: 'English'
ar: 'Arabic' ar: 'Arabic'
posts: posts:
prev: Previous page
next: Next page
empty: "There are no results for those search parameters." empty: "There are no results for those search parameters."
caption: Post list caption: Post list
attribute_ro: attribute_ro:
@ -413,6 +446,8 @@ en:
destroy: Remove image destroy: Remove image
belongs_to: belongs_to:
empty: "(Empty)" empty: "(Empty)"
predefined_value:
empty: "(Empty)"
draft: draft:
label: Draft label: Draft
reorder: reorder:
@ -535,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

@ -19,8 +19,8 @@ es:
remember_me: 'Mantiene la sesión abierta por %{remember_for}' remember_me: 'Mantiene la sesión abierta por %{remember_for}'
actions: actions:
sr-help: 'Después del formulario encontrarás vínculos para recuperar tu cuenta, entre otras acciones.' sr-help: 'Después del formulario encontrarás vínculos para recuperar tu cuenta, entre otras acciones.'
_true: _true: 'Sí'
_false: No _false: 'No'
svg: svg:
sutty: sutty:
title: Sutty title: Sutty
@ -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'
@ -163,7 +165,7 @@ es:
signature: 'Con cariño, Sutty' signature: 'Con cariño, Sutty'
breadcrumb: breadcrumb:
title: 'Tu ubicación en Sutty' title: 'Tu ubicación en Sutty'
logout: Salir logout: Cerrar sesión
mutual_aid: Ayuda mutua mutual_aid: Ayuda mutua
collaborations: collaborations:
collaborate: collaborate:
@ -180,6 +182,7 @@ es:
category: 'Categoría' category: 'Categoría'
sites: sites:
index: 'Este es el listado de sitios que puedes editar.' index: 'Este es el listado de sitios que puedes editar.'
edit_posts: 'Aquí verás el listado de todos los artículos y podrás editarlos o crear nuevos'
enqueued: 'El sitio está en la cola de espera para ser generado. enqueued: 'El sitio está en la cola de espera para ser generado.
Una vez que este proceso termine, recibirás un correo indicando el Una vez que este proceso termine, recibirás un correo indicando el
estado y si todo fue bien, se publicarán los cambios en tu sitio estado y si todo fue bien, se publicarán los cambios en tu sitio
@ -256,9 +259,38 @@ es:
help: | help: |
Las estadísticas visibilizan información sobre cómo se genera y Las estadísticas visibilizan información sobre cómo se genera y
cuántos recursos utiliza tu sitio. cuántos recursos utiliza tu sitio.
build: last_update: 'Actualizadas cada hora. Última actualización hace '
average: 'Tiempo promedio de generación' empty: 'Todavía no hay información suficiente. Te invitamos a volver en %{please_return_at} :)'
maximum: 'Tiempo máximo de generación' loading: 'Cargando...'
hour: 'Por hora'
day: 'Diarias'
week: 'Semanales'
month: 'Mensuales'
year: 'Anuales'
host:
title:
zero: 'Visitas del sitio'
one: 'Visitas del sitio'
other: 'Visitas agrupadas por nombre de dominio del sitio'
description: 'Cuenta la cantidad de páginas visitadas en tu sitio.'
urls:
title: 'Visitas por dirección'
description: 'Cantidad de visitas o descargas por dirección.'
label: 'Direcciones web (URL, "links", vínculos)'
help: 'Copia y pega una dirección por línea.'
submit: 'Actualizar gráfico'
resources:
title: 'Uso de recursos'
description: 'En esta sección podrás acceder a estadísticas del uso de recursos compartidos con otros sitios alojados en Sutty.'
builds:
title: 'Publicaciones del sitio'
description: 'Cantidad de veces que publicaste tu sitio.'
space_used:
title: 'Espacio utilizado en el servidor'
description: 'Espacio en disco que ocupa en promedio tu sitio.'
build_time:
title: 'Tiempo de publicación'
description: 'Tiempo promedio que toma en publicarse tu sitio.'
sites: sites:
donations: donations:
url: 'https://donaciones.sutty.nl/' url: 'https://donaciones.sutty.nl/'
@ -383,6 +415,8 @@ es:
en: 'inglés' en: 'inglés'
ar: 'árabe' ar: 'árabe'
posts: posts:
prev: Página anterior
next: Página siguiente
empty: No hay artículos con estos parámetros de búsqueda. empty: No hay artículos con estos parámetros de búsqueda.
caption: Lista de artículos caption: Lista de artículos
attribute_ro: attribute_ro:
@ -420,6 +454,8 @@ es:
destroy: 'Eliminar imagen' destroy: 'Eliminar imagen'
belongs_to: belongs_to:
empty: "(Vacío)" empty: "(Vacío)"
predefined_value:
empty: "(Vacío)"
draft: draft:
label: Borrador label: Borrador
reorder: reorder:
@ -542,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

@ -57,9 +57,10 @@ Rails.application.routes.draw do
# Gestionar artículos según idioma # Gestionar artículos según idioma
nested do nested do
scope '(:locale)' do scope '/(:locale)', constraint: /[a-z]{2}/ do
post :'posts/reorder', to: 'posts#reorder' post :'posts/reorder', to: 'posts#reorder'
resources :posts do resources :posts do
get 'p/:page', action: :index, on: :collection
get :preview, to: 'posts#preview' get :preview, to: 'posts#preview'
end end
end end
@ -75,5 +76,8 @@ Rails.application.routes.draw do
post 'reorder_posts', to: 'sites#reorder_posts' post 'reorder_posts', to: 'sites#reorder_posts'
resources :stats, only: [:index] resources :stats, only: [:index]
get :'stats/host', to: 'stats#host'
get :'stats/uris', to: 'stats#uris'
get :'stats/resources', to: 'stats#resources'
end end
end end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
# Agrega la columna de request_uri a la tabla de logs
class AddRequestUriToAccessLogs < ActiveRecord::Migration[6.1]
def change
return unless Rails.env.production?
add_column :access_logs, :request_uri, :string, default: ''
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
# Crear la tabla de Rollups
class CreateRollups < ActiveRecord::Migration[6.1]
def change
create_table :rollups do |t|
t.string :name, null: false
t.string :interval, null: false
t.datetime :time, null: false
t.jsonb :dimensions, null: false, default: {}
t.float :value
end
add_index :rollups, %i[name interval time dimensions], unique: true
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
# Cambia los msec a datetime para poder agregar por tiempos
class AddCreateAtToAccessLogs < ActiveRecord::Migration[6.1]
def up
add_column :access_logs, :created_at, :datetime, precision: 6
create_trigger(compatibility: 1).on(:access_logs).before(:insert) do
'new.created_at := to_timestamp(new.msec)'
end
ActiveRecord::Base.connection.execute('update access_logs set created_at = to_timestamp(msec);')
end
def down
remove_column :access_logs, :created_at
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
# Agrega índices únicos que pensábamos que ya existían.
class AddUniqueness < ActiveRecord::Migration[6.1]
def change
add_index :designs, :name, unique: true
add_index :designs, :gem, unique: true
add_index :licencias, :name, unique: true
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Una tabla que lleva el recuento de recolección de estadísticas, solo
# es necesario para saber cuándo se hicieron, si se hicieron y usar como
# caché.
class CreateStats < ActiveRecord::Migration[6.1]
def change
create_table :stats do |t|
t.timestamps
end
end
end

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

View file

@ -14,6 +14,8 @@
"@rails/webpacker": "5.2.1", "@rails/webpacker": "5.2.1",
"@suttyweb/editor": "^0.1.3", "@suttyweb/editor": "^0.1.3",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"chart.js": "^3.5.1",
"chartkick": "^4.0.5",
"circular-dependency-plugin": "^5.2.2", "circular-dependency-plugin": "^5.2.2",
"commonmark": "^0.29.0", "commonmark": "^0.29.0",
"fork-awesome": "^1.1.7", "fork-awesome": "^1.1.7",

View file

@ -2124,6 +2124,25 @@ chalk@^4.1.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
chart.js@>=3.0.2, chart.js@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.5.1.tgz#73e24d23a4134a70ccdb5e79a917f156b6f3644a"
integrity sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ==
chartjs-adapter-date-fns@>=2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz#5e53b2f660b993698f936f509c86dddf9ed44c6b"
integrity sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==
chartkick@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/chartkick/-/chartkick-4.0.5.tgz#310a60c931e8ceedc39adee2ef8e9d1e474cb0e6"
integrity sha512-xKak4Fsgfvp1hj/LykRKkniDMaZASx2A4TdVc/sfsiNFFNf1m+D7PGwP1vgj1UsbsCjOCSfGWWyJpOYxkUCBug==
optionalDependencies:
chart.js ">=3.0.2"
chartjs-adapter-date-fns ">=2.0.0"
date-fns ">=2.0.0"
chokidar@^2.1.8: chokidar@^2.1.8:
version "2.1.8" version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@ -2749,6 +2768,11 @@ dashdash@^1.12.0:
dependencies: dependencies:
assert-plus "^1.0.0" assert-plus "^1.0.0"
date-fns@>=2.0.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.24.0.tgz#7d86dc0d93c87b76b63d213b4413337cfd1c105d"
integrity sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw==
debug@2.6.9, debug@^2.2.0, debug@^2.3.3: debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
version "2.6.9" version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"