Merge branch 'origin-referer' into staging
This commit is contained in:
commit
d151fffdd2
45 changed files with 1017 additions and 350 deletions
|
@ -11,20 +11,58 @@ module Api
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Realiza la inversa de Site#hostname
|
# Por retrocompatibilidad con la forma en que estábamos
|
||||||
|
# gestionando los hostnames históricamente, necesitamos poder
|
||||||
|
# encontrar el sitio a partir de cualquiera de sus hostnames.
|
||||||
#
|
#
|
||||||
# TODO: El sitio sutty.nl no aplica a ninguno de estos y le
|
# Aunque en realidad con el hostname a partir del Origin nos
|
||||||
# tuvimos que poner 'sutty.nl..sutty.nl' para pasar el test.
|
# bastaría.
|
||||||
|
#
|
||||||
|
# TODO: Generar API v2 que use solo el hostname y no haya que
|
||||||
|
# pasar site_id como parámetro redundante.
|
||||||
def site_id
|
def site_id
|
||||||
@site_id ||= if params[:site_id].end_with? Site.domain
|
@site_id ||= Deploy.site_name_from_hostname(params[:site_id])
|
||||||
params[:site_id].sub(/\.#{Site.domain}\z/, '')
|
|
||||||
else
|
|
||||||
params[:site_id] + '.'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [Site]
|
||||||
|
def site
|
||||||
|
@site ||= Site.find_by_name(site_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Obtiene el hostname desde el Origin, con el hostname local como
|
||||||
|
# fallback.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def origin_hostname
|
||||||
|
URI.parse(origin || origin_from_referer).host
|
||||||
|
rescue StandardError
|
||||||
|
"#{site_id}.#{Site.domain}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Referer
|
||||||
|
#
|
||||||
|
# @see {https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer}
|
||||||
|
# @return [String,Nil]
|
||||||
|
def referer
|
||||||
|
request.referer
|
||||||
|
end
|
||||||
|
alias referrer referer
|
||||||
|
|
||||||
|
# Origin
|
||||||
|
#
|
||||||
|
# @see {https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin}
|
||||||
|
# @return [String,Nil]
|
||||||
def origin
|
def origin
|
||||||
request.headers['Origin']
|
request.origin
|
||||||
|
end
|
||||||
|
|
||||||
|
# Genera un header Origin a partir del Referer si existe.
|
||||||
|
#
|
||||||
|
# @return [String,Nil]
|
||||||
|
def origin_from_referer
|
||||||
|
return if referer.blank?
|
||||||
|
|
||||||
|
referer.split('/', 4).tap { |u| u.pop if u.size > 3 }.join('/')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Los navegadores antiguos no envían Origin
|
# Los navegadores antiguos no envían Origin
|
||||||
|
|
|
@ -23,7 +23,7 @@ module Api
|
||||||
contact_params.to_h.symbolize_keys,
|
contact_params.to_h.symbolize_keys,
|
||||||
params[:redirect]
|
params[:redirect]
|
||||||
|
|
||||||
redirect_to params[:redirect] || origin.to_s
|
redirect_to params[:redirect] || referer || site.url
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -44,11 +44,10 @@ module Api
|
||||||
|
|
||||||
# Genera el Origin correcto a partir de la URL del sitio.
|
# Genera el Origin correcto a partir de la URL del sitio.
|
||||||
#
|
#
|
||||||
# En desarrollo devuelve el Origin enviado.
|
# @see {https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin}
|
||||||
#
|
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def return_origin
|
def return_origin
|
||||||
Rails.env.production? ? Site.find_by(name: site_id).url : origin
|
site&.deploys&.find_by_hostname(origin_hostname)&.url
|
||||||
end
|
end
|
||||||
|
|
||||||
# La cookie no es accesible a través de JS y todo su contenido
|
# La cookie no es accesible a través de JS y todo su contenido
|
||||||
|
@ -59,6 +58,8 @@ module Api
|
||||||
# TODO: Volver configurable por sitio
|
# TODO: Volver configurable por sitio
|
||||||
expires = ENV.fetch('COOKIE_DURATION', '30').to_i.minutes
|
expires = ENV.fetch('COOKIE_DURATION', '30').to_i.minutes
|
||||||
|
|
||||||
|
# TODO: ¿Son necesarios estos headers en la descarga de una
|
||||||
|
# imagen? ¿No será mejor moverlos al envío de datos?
|
||||||
headers['Access-Control-Allow-Origin'] = return_origin
|
headers['Access-Control-Allow-Origin'] = return_origin
|
||||||
headers['Access-Control-Allow-Credentials'] = true
|
headers['Access-Control-Allow-Credentials'] = true
|
||||||
headers['Vary'] = 'Origin'
|
headers['Vary'] = 'Origin'
|
||||||
|
|
|
@ -17,7 +17,7 @@ module Api
|
||||||
site.touch if service.create_anonymous.persisted?
|
site.touch if service.create_anonymous.persisted?
|
||||||
|
|
||||||
# Redirigir a la URL de agradecimiento
|
# Redirigir a la URL de agradecimiento
|
||||||
redirect_to params[:redirect_to] || origin.to_s
|
redirect_to params[:redirect_to] || referer || site.url
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -85,7 +85,9 @@ module Api
|
||||||
# XXX: Este header se puede falsificar de todas formas pero al
|
# XXX: Este header se puede falsificar de todas formas pero al
|
||||||
# menos es una trampa.
|
# menos es una trampa.
|
||||||
def site_is_origin?
|
def site_is_origin?
|
||||||
return if origin? && site.urls(slash: false).any? { |u| origin.to_s.start_with? u }
|
return if site.urls(slash: false).any? do |u|
|
||||||
|
(origin || origin_from_referer).to_s.start_with? u
|
||||||
|
end
|
||||||
|
|
||||||
@reason = 'site_is_not_origin'
|
@reason = 'site_is_not_origin'
|
||||||
render plain: Rails.env.production? ? nil : @reason, status: :precondition_required
|
render plain: Rails.env.production? ? nil : @reason, status: :precondition_required
|
||||||
|
@ -116,11 +118,6 @@ module Api
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
# Encuentra el sitio o devuelve nulo
|
|
||||||
def site
|
|
||||||
@site ||= Site.find_by(name: site_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Genera un registro con información básica para debug, quizás no
|
# Genera un registro con información básica para debug, quizás no
|
||||||
# quede asociado a ningún sitio.
|
# quede asociado a ningún sitio.
|
||||||
#
|
#
|
||||||
|
|
|
@ -9,14 +9,14 @@ module Api
|
||||||
|
|
||||||
# Lista de nombres de dominios a emitir certificados
|
# Lista de nombres de dominios a emitir certificados
|
||||||
def index
|
def index
|
||||||
render json: sites_names + alternative_names + api_names
|
render json: Deploy.all.pluck(:hostname)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sitios con hidden service de Tor
|
# Sitios con hidden service de Tor
|
||||||
#
|
#
|
||||||
# @return [Array] lista de nombres de sitios sin onion aun
|
# @return [Array] lista de nombres de sitios sin onion aun
|
||||||
def hidden_services
|
def hidden_services
|
||||||
render json: DeployHiddenService.where(values: nil).includes(:site).pluck(:name)
|
render json: DeployHiddenService.temporary.includes(:site).pluck(:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Tor va a enviar el onion junto con el nombre del sitio y tenemos
|
# Tor va a enviar el onion junto con el nombre del sitio y tenemos
|
||||||
|
@ -25,10 +25,8 @@ module Api
|
||||||
# @params [String] name
|
# @params [String] name
|
||||||
# @params [String] onion
|
# @params [String] onion
|
||||||
def add_onion
|
def add_onion
|
||||||
site = Site.find_by(name: params[:name])
|
if (site = Site.find_by_name(params[:name]))
|
||||||
|
usuarie = GitAuthor.new email: "tor@#{Site.domain}", name: 'Tor'
|
||||||
if site
|
|
||||||
usuarie = GitAuthor.new email: 'tor@' + Site.domain, name: 'Tor'
|
|
||||||
service = SiteService.new site: site, usuarie: usuarie,
|
service = SiteService.new site: site, usuarie: usuarie,
|
||||||
params: params
|
params: params
|
||||||
service.add_onion
|
service.add_onion
|
||||||
|
@ -36,28 +34,6 @@ module Api
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Nombres de los sitios
|
|
||||||
def sites_names
|
|
||||||
Site.all.order(:name).pluck(:name)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Dominios alternativos
|
|
||||||
def alternative_names
|
|
||||||
DeployAlternativeDomain.all.map(&:hostname)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Obtener todos los sitios con API habilitada, es decir formulario
|
|
||||||
# de contacto y/o colaboración anónima.
|
|
||||||
#
|
|
||||||
# TODO: Optimizar
|
|
||||||
def api_names
|
|
||||||
Site.where(contact: true)
|
|
||||||
.or(Site.where(colaboracion_anonima: true))
|
|
||||||
.select("'api.' || name as name").map(&:name)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
before_action :prepare_exception_notifier
|
before_action :prepare_exception_notifier
|
||||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||||
|
before_action :redirect_to_site_name!, only: %i[index show edit new], if: :site_id_contains_hostname?
|
||||||
around_action :set_locale
|
around_action :set_locale
|
||||||
|
|
||||||
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
||||||
|
@ -16,7 +17,7 @@ class ApplicationController < ActionController::Base
|
||||||
rescue_from ActionController::ParameterMissing, with: :page_not_found
|
rescue_from ActionController::ParameterMissing, with: :page_not_found
|
||||||
|
|
||||||
before_action do
|
before_action do
|
||||||
Rack::MiniProfiler.authorize_request if current_usuarie&.email&.ends_with?('@' + ENV.fetch('SUTTY', 'sutty.nl'))
|
Rack::MiniProfiler.authorize_request if Rails.env.development?
|
||||||
end
|
end
|
||||||
|
|
||||||
# No tenemos índice de sutty, vamos directamente a ver el listado de
|
# No tenemos índice de sutty, vamos directamente a ver el listado de
|
||||||
|
@ -31,15 +32,11 @@ class ApplicationController < ActionController::Base
|
||||||
/[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}/ =~ string
|
/[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}/ =~ string
|
||||||
end
|
end
|
||||||
|
|
||||||
# Encontrar un sitio por su nombre
|
# Encontrar un sitio por su nombre.
|
||||||
def find_site
|
def find_site
|
||||||
id = params[:site_id] || params[:id]
|
current_usuarie&.sites&.find_by_name(site_id).tap do |site|
|
||||||
|
raise SiteNotFound unless site
|
||||||
unless (site = current_usuarie&.sites&.find_by_name(id))
|
|
||||||
raise SiteNotFound
|
|
||||||
end
|
end
|
||||||
|
|
||||||
site
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Devuelve el idioma actual y si no lo encuentra obtiene uno por
|
# Devuelve el idioma actual y si no lo encuentra obtiene uno por
|
||||||
|
@ -79,6 +76,39 @@ class ApplicationController < ActionController::Base
|
||||||
breadcrumb 'stats.index', root_path, match: :exact
|
breadcrumb 'stats.index', root_path, match: :exact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Retrocompatibilidad con sitios cuyo nombre era su hostname.
|
||||||
|
#
|
||||||
|
# @see Deploy
|
||||||
|
def site_id_contains_hostname?
|
||||||
|
site_id&.end_with? '.'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Redirigir a la misma URL con el site_id cambiado.
|
||||||
|
#
|
||||||
|
# TODO: Eliminar cuando detectemos que no hay más redirecciones.
|
||||||
|
def redirect_to_site_name!
|
||||||
|
params.permit!
|
||||||
|
params[:site_id] = Deploy.site_name_from_hostname(site_id[0..-2])
|
||||||
|
|
||||||
|
redirect_to params, status: :moved_permanently
|
||||||
|
end
|
||||||
|
|
||||||
|
# Los controladores dentro de SitesController van a usar site_id
|
||||||
|
# mientras que SiteController va a usar ID.
|
||||||
|
#
|
||||||
|
# @see SitesController
|
||||||
|
# @return [String,Nil]
|
||||||
|
def site_id
|
||||||
|
@site_id ||= params[:site_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
# El sitio actual
|
||||||
|
#
|
||||||
|
# @return [Site]
|
||||||
|
def site
|
||||||
|
@site ||= find_site
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def configure_permitted_parameters
|
def configure_permitted_parameters
|
||||||
|
|
|
@ -7,7 +7,7 @@ class CollaborationsController < ApplicationController
|
||||||
include Pundit
|
include Pundit
|
||||||
|
|
||||||
def collaborate
|
def collaborate
|
||||||
@site = Site.find_by_name(params[:site_id])
|
@site = find_site
|
||||||
authorize Collaboration.new(@site)
|
authorize Collaboration.new(@site)
|
||||||
|
|
||||||
@invitade = current_usuarie || @site.usuaries.build
|
@invitade = current_usuarie || @site.usuaries.build
|
||||||
|
@ -21,7 +21,7 @@ class CollaborationsController < ApplicationController
|
||||||
#
|
#
|
||||||
# * Si le usuarie existe y no está logueade, pedirle la contraseña
|
# * Si le usuarie existe y no está logueade, pedirle la contraseña
|
||||||
def accept_collaboration
|
def accept_collaboration
|
||||||
@site = Site.find_by_name(params[:site_id])
|
@site = find_site
|
||||||
authorize Collaboration.new(@site)
|
authorize Collaboration.new(@site)
|
||||||
|
|
||||||
@invitade = current_usuarie
|
@invitade = current_usuarie
|
||||||
|
|
|
@ -136,8 +136,10 @@ class SitesController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def site
|
# En los controladores dentro de este controlador vamos a usar :id
|
||||||
@site ||= find_site
|
# para obtener el nombre.
|
||||||
|
def site_id
|
||||||
|
@site_id ||= params[:site_id] || params[:id]
|
||||||
end
|
end
|
||||||
|
|
||||||
def site_params
|
def site_params
|
||||||
|
|
|
@ -4,70 +4,63 @@
|
||||||
class DeployJob < ApplicationJob
|
class DeployJob < ApplicationJob
|
||||||
class DeployException < StandardError; end
|
class DeployException < StandardError; end
|
||||||
|
|
||||||
|
attr_reader :site, :deployed
|
||||||
|
|
||||||
# rubocop:disable Metrics/MethodLength
|
# rubocop:disable Metrics/MethodLength
|
||||||
def perform(site, notify = true, time = Time.now)
|
def perform(site_id, 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_id)
|
||||||
|
@deployed = {}
|
||||||
|
|
||||||
# Si ya hay una tarea corriendo, aplazar esta. Si estuvo
|
# Si ya hay una tarea corriendo, aplazar esta. Si estuvo
|
||||||
# esperando más de 10 minutos, recuperar el estado anterior.
|
# esperando más de 10 minutos, recuperar el estado anterior.
|
||||||
#
|
#
|
||||||
# Como el trabajo actual se aplaza al siguiente, arrastrar la
|
# Como el trabajo actual se aplaza al siguiente, arrastrar la
|
||||||
# hora original para poder ir haciendo timeouts.
|
# hora original para poder ir haciendo timeouts.
|
||||||
if @site.building?
|
if site.building?
|
||||||
if 10.minutes.ago >= time
|
if 10.minutes.ago >= time
|
||||||
@site.update status: 'waiting'
|
site.update status: 'waiting'
|
||||||
raise DeployException,
|
raise DeployException,
|
||||||
"#{@site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
|
"#{site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
|
||||||
end
|
end
|
||||||
|
|
||||||
DeployJob.perform_in(60, site, notify, time)
|
DeployJob.perform_in(60, site_id, notify, time)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@site.update status: 'building'
|
site.update status: 'building'
|
||||||
# Asegurarse que DeployLocal sea el primero!
|
# Asegurarse que DeployLocal sea el primero!
|
||||||
@deployed = { deploy_local: deploy_locally }
|
deployed[:deploy_local] = site.deploy_local.deploy
|
||||||
|
|
||||||
# No es opcional
|
deploy_others if deployed[:deploy_local]
|
||||||
unless @deployed[:deploy_local]
|
|
||||||
@site.update status: 'waiting'
|
|
||||||
notify_usuaries if notify
|
|
||||||
|
|
||||||
# Hacer fallar la tarea
|
|
||||||
raise DeployException, deploy_local.build_stats.last.log
|
|
||||||
end
|
|
||||||
|
|
||||||
deploy_others
|
|
||||||
|
|
||||||
# Volver a la espera
|
# Volver a la espera
|
||||||
@site.update status: 'waiting'
|
site.update status: 'waiting'
|
||||||
|
|
||||||
notify_usuaries if notify
|
notify_usuaries if notify
|
||||||
|
|
||||||
|
# Hacer fallar la tarea para enterarnos.
|
||||||
|
raise DeployException, site.deploy_local.build_stats.last.log unless deployed[:deploy_local]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/MethodLength
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def deploy_local
|
# Correr todas las tareas que no sean el deploy local.
|
||||||
@deploy_local ||= @site.deploys.find_by(type: 'DeployLocal')
|
|
||||||
end
|
|
||||||
|
|
||||||
def deploy_locally
|
|
||||||
deploy_local.deploy
|
|
||||||
end
|
|
||||||
|
|
||||||
def deploy_others
|
def deploy_others
|
||||||
@site.deploys.where.not(type: 'DeployLocal').find_each do |d|
|
site.deploys.where.not(type: 'DeployLocal').find_each do |d|
|
||||||
@deployed[d.type.underscore.to_sym] = d.deploy
|
deployed[d.type.underscore.to_sym] = d.deploy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Notificar a todes les usuaries no temporales.
|
||||||
|
#
|
||||||
|
# TODO: Poder configurar quiénes quieren recibir notificaciones.
|
||||||
def notify_usuaries
|
def notify_usuaries
|
||||||
@site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie|
|
site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie|
|
||||||
DeployMailer.with(usuarie: usuarie, site: @site.id)
|
DeployMailer.with(usuarie: usuarie, site: site.id)
|
||||||
.deployed(@deployed)
|
.deployed(deployed)
|
||||||
.deliver_now
|
.deliver_now
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ class DeployMailer < ApplicationMailer
|
||||||
@usuarie = Usuarie.find(params[:usuarie])
|
@usuarie = Usuarie.find(params[:usuarie])
|
||||||
@site = @usuarie.sites.find(params[:site])
|
@site = @usuarie.sites.find(params[:site])
|
||||||
@deploys = which_ones
|
@deploys = which_ones
|
||||||
@deploy_local = @site.deploys.find_by(type: 'DeployLocal')
|
@deploy_local = @site.deploy_local
|
||||||
|
|
||||||
# Informamos a cada quien en su idioma y damos una dirección de
|
# Informamos a cada quien en su idioma y damos una dirección de
|
||||||
# respuesta porque a veces les usuaries nos escriben
|
# respuesta porque a veces les usuaries nos escriben
|
||||||
|
|
|
@ -1,44 +1,138 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'open3'
|
require 'open3'
|
||||||
|
|
||||||
# Este modelo implementa los distintos tipos de alojamiento que provee
|
# Este modelo implementa los distintos tipos de alojamiento que provee
|
||||||
# Sutty.
|
# Sutty.
|
||||||
#
|
#
|
||||||
# Los datos se guardan en la tabla `deploys`. Para guardar los
|
# Cuando cambia el hostname de un Deploy, generamos un
|
||||||
# atributos, cada modelo tiene que definir su propio `store
|
# DeployAlternativeDomain en su lugar. Esto permite que no se rompan
|
||||||
# :attributes`.
|
# links preexistentes y que el nombre no pueda ser tomado por alguien
|
||||||
|
# más.
|
||||||
|
#
|
||||||
|
# TODO: Cambiar el nombre a algo que no sea industrial/militar.
|
||||||
class Deploy < ApplicationRecord
|
class Deploy < ApplicationRecord
|
||||||
|
# Un sitio puede tener muchas formas de publicarse.
|
||||||
belongs_to :site
|
belongs_to :site
|
||||||
|
# Puede tener muchos access logs a través del hostname
|
||||||
|
has_many :access_logs, primary_key: 'hostname', foreign_key: 'host'
|
||||||
|
# Registro de las tareas ejecutadas
|
||||||
has_many :build_stats, dependent: :destroy
|
has_many :build_stats, dependent: :destroy
|
||||||
|
|
||||||
|
# Siempre generar el hostname
|
||||||
|
after_initialize :default_hostname!
|
||||||
|
# Eliminar los archivos generados por el deploy.
|
||||||
|
before_destroy :remove_destination!
|
||||||
|
# Cambiar el lugar del destino antes de guardar los cambios, para que
|
||||||
|
# el hostname anterior siga estando disponible.
|
||||||
|
before_update :rename_destination!, if: :destination_changed?
|
||||||
|
# Los hostnames alternativos se crean después de actualizar, cuando ya
|
||||||
|
# se modificó el hostname.
|
||||||
|
around_update :create_alternative_domain!, if: :destination_changed?
|
||||||
|
|
||||||
|
# Siempre tienen que pertenecer a un sitio
|
||||||
|
validates :site, presence: true
|
||||||
|
# El hostname tiene que ser único en toda la plataforma
|
||||||
|
validates :hostname, uniqueness: true
|
||||||
|
# Cada deploy puede implementar su propia validación
|
||||||
|
validates :hostname, hostname: true, unless: :implements_hostname_validation?
|
||||||
|
# Verificar que se puede cambiar de lugar el destino y no hay nada
|
||||||
|
# preexistente.
|
||||||
|
validate :destination_can_change?, if: :destination_changed?
|
||||||
|
|
||||||
|
# Retrocompatibilidad: Encuentra el site_name a partir del hostname.
|
||||||
|
#
|
||||||
|
# @return [String,Nil]
|
||||||
|
def self.site_name_from_hostname(hostname)
|
||||||
|
where(hostname: hostname).includes(:site).pluck(:name).first
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si el destino existe y si no es un symlink roto.
|
||||||
|
def exist?
|
||||||
|
File.exist? destination
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si el link está roto
|
||||||
|
def broken?
|
||||||
|
File.symlink?(destination) && !File.exist?(File.readlink(destination))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Ubicación del deploy
|
||||||
|
#
|
||||||
|
# @return [String] Una ruta en el sistema de archivos
|
||||||
|
def destination
|
||||||
|
File.join(Rails.root, '_deploy', hostname)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Ubicación anterior del deploy
|
||||||
|
#
|
||||||
|
# @return [String] Una ruta en el sistema de archivos
|
||||||
|
def destination_was
|
||||||
|
return destination unless will_save_change_to_hostname?
|
||||||
|
|
||||||
|
File.join(Rails.root, '_deploy', hostname_was)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Determina si la ubicación cambió
|
||||||
|
def destination_changed?
|
||||||
|
persisted? && will_save_change_to_hostname?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Genera el hostname
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def default_hostname
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
# Devolver la URL
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def url
|
||||||
|
"https://#{hostname}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Ejecutar la tarea
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
def deploy
|
def deploy
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
def limit
|
# El espacio ocupado por este deploy.
|
||||||
raise NotImplementedError
|
#
|
||||||
end
|
# @return [Integer]
|
||||||
|
|
||||||
def size
|
def size
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Empezar a contar el tiempo
|
||||||
|
#
|
||||||
|
# @return [Time]
|
||||||
def time_start
|
def time_start
|
||||||
@start = Time.now
|
@start = Time.now
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Detener el contador
|
||||||
|
#
|
||||||
|
# @return [Time]
|
||||||
def time_stop
|
def time_stop
|
||||||
@stop = Time.now
|
@stop = Time.now
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Obtener la demora de la tarea
|
||||||
|
#
|
||||||
|
# @return [Float]
|
||||||
def time_spent_in_seconds
|
def time_spent_in_seconds
|
||||||
(@stop - @start).round(3)
|
(@stop - @start).round(3)
|
||||||
end
|
end
|
||||||
|
|
||||||
def home_dir
|
# El directorio donde se almacenan las gemas.
|
||||||
site.path
|
#
|
||||||
end
|
# TODO: En un momento podíamos tenerlas todas compartidas y ahorrar
|
||||||
|
# espacio, pero bundler empezó a mezclar cosas.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
def gems_dir
|
def gems_dir
|
||||||
@gems_dir ||= Rails.root.join('_storage', 'gems', site.name)
|
@gems_dir ||= Rails.root.join('_storage', 'gems', site.name)
|
||||||
end
|
end
|
||||||
|
@ -77,9 +171,67 @@ class Deploy < ApplicationRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# Genera el hostname pero permitir la inicialización del valor. Luego
|
||||||
|
# validamos que sea el formato correcto.
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def default_hostname!
|
||||||
|
self.hostname ||= default_hostname
|
||||||
|
end
|
||||||
|
|
||||||
|
# Cambia la ubicación de destino cuando cambia el hostname.
|
||||||
|
def rename_destination!
|
||||||
|
return unless File.exist? destination_was
|
||||||
|
|
||||||
|
FileUtils.mv destination_was, destination
|
||||||
|
end
|
||||||
|
|
||||||
|
# Elimina los archivos generados por el deploy
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def remove_destination!
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
# Cuando el deploy cambia de hostname, generamos un dominio
|
||||||
|
# alternativo para no romper links hacia este sitio.
|
||||||
|
def create_alternative_domain!
|
||||||
|
hw = hostname_was
|
||||||
|
|
||||||
|
# Aplicar la actualización
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Crear el deploy alternativo con el nombre anterior una vez que
|
||||||
|
# lo cambiamos en la base de datos.
|
||||||
|
ad = site.deploys.create(type: 'DeployAlternativeDomain', hostname: hw)
|
||||||
|
ad.deploy if ad.persisted?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Devuelve un error si el destino ya existe. No debería fallar si ya
|
||||||
|
# pasamos la validación de cambio de nombres, pero siempre puede haber
|
||||||
|
# directorios y links sueltos.
|
||||||
|
def destination_can_change?
|
||||||
|
return true unless persisted?
|
||||||
|
|
||||||
|
remove_destination! if broken?
|
||||||
|
|
||||||
|
return true unless exist?
|
||||||
|
|
||||||
|
errors.add :hostname, I18n.t('activerecord.errors.models.deploy.attributes.hostname.destination_exist')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Convierte el comando en una versión resumida.
|
||||||
|
#
|
||||||
# @param [String]
|
# @param [String]
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def readable_cmd(cmd)
|
def readable_cmd(cmd)
|
||||||
cmd.split(' -', 2).first.tr(' ', '_')
|
cmd.split(' -', 2).first.tr(' ', '_')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Cada deploy puede decidir su propia validación
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def implements_hostname_validation?
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Soportar dominios alternativos
|
# Soportar dominios alternativos.
|
||||||
class DeployAlternativeDomain < Deploy
|
class DeployAlternativeDomain < DeployWww
|
||||||
store :values, accessors: %i[hostname], coder: JSON
|
validates :hostname, domainname: true
|
||||||
|
|
||||||
# Generar un link simbólico del sitio principal al alternativo
|
# No hay un hostname por defecto
|
||||||
def deploy
|
#
|
||||||
File.symlink?(destination) ||
|
# @return [Nil]
|
||||||
File.symlink(site.hostname, destination).zero?
|
def default_hostname; end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def implements_hostname_validation?
|
||||||
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
# No hay límite para los dominios alternativos
|
# No hay un hostname por defecto. Debe ser informado por les
|
||||||
def limit; end
|
# usuaries.
|
||||||
|
def default_hostname!; end
|
||||||
def size
|
|
||||||
File.size destination
|
|
||||||
end
|
|
||||||
|
|
||||||
def destination
|
|
||||||
File.join(Rails.root, '_deploy', hostname.gsub(/\.\z/, ''))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,18 +1,61 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Genera una versión onion
|
# Alojar el sitio como un servicio oculto de Tor, que en realidad es un
|
||||||
|
# link simbólico al DeployLocal.
|
||||||
class DeployHiddenService < DeployWww
|
class DeployHiddenService < DeployWww
|
||||||
def deploy
|
validates :hostname, format: { with: /\A[a-z2-7]{56}.onion\z/ }
|
||||||
return true if fqdn.blank?
|
|
||||||
|
|
||||||
super
|
# Sufijo para todos los dominios temporales.
|
||||||
end
|
TEMPORARY_SUFFIX = 'temporary'
|
||||||
|
|
||||||
def fqdn
|
# Traer todos los servicios ocultos temporales.
|
||||||
values[:onion]
|
scope :temporary, -> { where("hostname not like '#{TEMPORARY_SUFFIX}%'") }
|
||||||
end
|
|
||||||
|
|
||||||
|
# Los servicios ocultos son su propio transporte cifrado y
|
||||||
|
# autenticado.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
def url
|
def url
|
||||||
'http://' + fqdn
|
"http://#{hostname}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Los onions no son creados por Sutty sino por Tor y enviados luego a
|
||||||
|
# través de la API. El hostname por defecto es un nombre temporal que
|
||||||
|
# se parece a una dirección OnionV3.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def default_hostname
|
||||||
|
"#{TEMPORARY_SUFFIX}#{random_base32}.onion"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si es una dirección temporal.
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def temporary?
|
||||||
|
hostname.start_with? TEMPORARY_SUFFIX
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# No soportamos cambiar de onion
|
||||||
|
def destination_changed?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def implements_hostname_validation?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adaptado de base32
|
||||||
|
#
|
||||||
|
# @see {https://github.com/stesla/base32/blob/master/lib/base32.rb}
|
||||||
|
# @see {https://github.com/stesla/base32/blob/master/LICENSE}
|
||||||
|
def random_base32(length = nil)
|
||||||
|
table = 'abcdefghijklmnopqrstuvwxyz234567'
|
||||||
|
length ||= 56 - TEMPORARY_SUFFIX.length
|
||||||
|
|
||||||
|
OpenSSL::Random.random_bytes(length).each_byte.map do |b|
|
||||||
|
table[b % 32]
|
||||||
|
end.join
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Alojamiento local, solo genera el sitio, con lo que no necesita hacer
|
# Alojamiento local, genera el sitio como si corriéramos `jekyll build`.
|
||||||
# nada más
|
|
||||||
class DeployLocal < Deploy
|
class DeployLocal < Deploy
|
||||||
store :values, accessors: %i[], coder: JSON
|
# Asegurarse que el hostname es el permitido.
|
||||||
|
before_validation :reset_hostname!, :default_hostname!
|
||||||
before_destroy :remove_destination!
|
# Actualiza el hostname con www si cambiamos el hostname
|
||||||
|
before_update :update_deploy_www!, if: :hostname_changed?
|
||||||
|
|
||||||
# Realizamos la construcción del sitio usando Jekyll y un entorno
|
# Realizamos la construcción del sitio usando Jekyll y un entorno
|
||||||
# limpio para no pasarle secretos
|
# limpio para no pasarle secretos
|
||||||
|
@ -20,42 +20,52 @@ class DeployLocal < Deploy
|
||||||
jekyll_build
|
jekyll_build
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sólo permitimos un deploy local
|
|
||||||
def limit
|
|
||||||
1
|
|
||||||
end
|
|
||||||
|
|
||||||
# Obtener el tamaño de todos los archivos y directorios (los
|
# Obtener el tamaño de todos los archivos y directorios (los
|
||||||
# directorios son archivos :)
|
# directorios son archivos :)
|
||||||
|
#
|
||||||
|
# @return [Integer]
|
||||||
def size
|
def size
|
||||||
paths = [destination, File.join(destination, '**', '**')]
|
paths = [destination, File.join(destination, '**', '**')]
|
||||||
|
|
||||||
Dir.glob(paths).map do |file|
|
Dir.glob(paths).map do |file|
|
||||||
if File.symlink? file
|
File.symlink?(file) ? 0 : File.size(file)
|
||||||
0
|
|
||||||
else
|
|
||||||
File.size(file)
|
|
||||||
end
|
|
||||||
end.inject(:+)
|
end.inject(:+)
|
||||||
end
|
end
|
||||||
|
|
||||||
def destination
|
# El hostname es el nombre del sitio más el dominio principal.
|
||||||
File.join(Rails.root, '_deploy', site.hostname)
|
#
|
||||||
|
# @return [String]
|
||||||
|
def default_hostname
|
||||||
|
"#{site.name}.#{Site.domain}"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def reset_hostname!
|
||||||
|
self.hostname = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# XXX: En realidad el DeployWww debería regenerar su propio hostname.
|
||||||
|
def update_deploy_www!
|
||||||
|
site.deploys.where(type: 'DeployWww').map do |www|
|
||||||
|
www.update hostname: www.default_hostname
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Crea el directorio destino si no existe.
|
||||||
def mkdir
|
def mkdir
|
||||||
FileUtils.mkdir_p destination
|
FileUtils.mkdir_p destination
|
||||||
end
|
end
|
||||||
|
|
||||||
# Un entorno que solo tiene lo que necesitamos
|
# Un entorno que solo tiene lo que necesitamos
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
def env
|
def env
|
||||||
# XXX: This doesn't support Windows paths :B
|
# XXX: This doesn't support Windows paths :B
|
||||||
paths = [File.dirname(`which bundle`), '/usr/bin', '/bin']
|
paths = [File.dirname(`which bundle`), '/usr/bin', '/bin']
|
||||||
|
|
||||||
{
|
{
|
||||||
'HOME' => home_dir,
|
'HOME' => site.path,
|
||||||
'PATH' => paths.join(':'),
|
'PATH' => paths.join(':'),
|
||||||
'SPREE_API_KEY' => site.tienda_api_key,
|
'SPREE_API_KEY' => site.tienda_api_key,
|
||||||
'SPREE_URL' => site.tienda_url,
|
'SPREE_URL' => site.tienda_url,
|
||||||
|
@ -66,10 +76,15 @@ class DeployLocal < Deploy
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
def yarn_lock
|
def yarn_lock
|
||||||
File.join(site.path, 'yarn.lock')
|
File.join(site.path, 'yarn.lock')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Determina si este proyecto se gestiona con Yarn, buscando si el
|
||||||
|
# archivo yarn.lock existe.
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
def yarn_lock?
|
def yarn_lock?
|
||||||
File.exist? yarn_lock
|
File.exist? yarn_lock
|
||||||
end
|
end
|
||||||
|
@ -79,12 +94,17 @@ class DeployLocal < Deploy
|
||||||
end
|
end
|
||||||
|
|
||||||
# Corre yarn dentro del repositorio
|
# Corre yarn dentro del repositorio
|
||||||
|
#
|
||||||
|
# @return [Boolean,Nil]
|
||||||
def yarn
|
def yarn
|
||||||
return true unless yarn_lock?
|
return true unless yarn_lock?
|
||||||
|
|
||||||
run 'yarn'
|
run 'yarn'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Instala las dependencias.
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
def bundle
|
def bundle
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
run %(bundle install --no-cache --path="#{gems_dir}")
|
run %(bundle install --no-cache --path="#{gems_dir}")
|
||||||
|
@ -93,6 +113,9 @@ class DeployLocal < Deploy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Genera el sitio.
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
def jekyll_build
|
def jekyll_build
|
||||||
run %(bundle exec jekyll build --trace --profile --destination "#{escaped_destination}")
|
run %(bundle exec jekyll build --trace --profile --destination "#{escaped_destination}")
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,12 +11,31 @@ class DeployPrivate < DeployLocal
|
||||||
jekyll_build
|
jekyll_build
|
||||||
end
|
end
|
||||||
|
|
||||||
# Hacer el deploy a un directorio privado
|
# La URL del sitio dentro del panel.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def url
|
||||||
|
Rails.application.routes.url_for(controller: :private, action: :show, site_id: site)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Hacer el deploy a un directorio privado.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
def destination
|
def destination
|
||||||
File.join(Rails.root, '_private', site.name)
|
File.join(Rails.root, '_private', site.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# El hostname no se usa para nada, porque el sitio es solo accesible a
|
||||||
|
# través del panel de Sutty.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def default_hostname
|
||||||
|
"#{site.name}.private.#{Site.domain}"
|
||||||
|
end
|
||||||
|
|
||||||
# No usar recursos en compresión y habilitar los datos privados
|
# No usar recursos en compresión y habilitar los datos privados
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
def env
|
def env
|
||||||
@env ||= super.merge({
|
@env ||= super.merge({
|
||||||
'JEKYLL_ENV' => 'development',
|
'JEKYLL_ENV' => 'development',
|
||||||
|
|
|
@ -2,34 +2,48 @@
|
||||||
|
|
||||||
# Vincula la versión del sitio con www a la versión sin
|
# Vincula la versión del sitio con www a la versión sin
|
||||||
class DeployWww < Deploy
|
class DeployWww < Deploy
|
||||||
store :values, accessors: %i[], coder: JSON
|
# La forma de hacer este deploy es generar un link simbólico entre el
|
||||||
|
# directorio canónico y el actual.
|
||||||
before_destroy :remove_destination!
|
#
|
||||||
|
# @return [Boolean]
|
||||||
def deploy
|
def deploy
|
||||||
File.symlink?(destination) ||
|
# Eliminar los links rotos
|
||||||
File.symlink(site.hostname, destination).zero?
|
remove_destination! if broken?
|
||||||
end
|
|
||||||
|
# No hacer nada si ya existe.
|
||||||
def limit
|
return true if exist?
|
||||||
1
|
|
||||||
|
# Generar un link simbólico con la ruta relativa al destino
|
||||||
|
File.symlink(relative_path, destination).zero?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Siempre devuelve el espacio ocupado por el link simbólico, no el
|
||||||
|
# destino.
|
||||||
|
#
|
||||||
|
# @return [Integer]
|
||||||
def size
|
def size
|
||||||
File.size destination
|
relative_path.size
|
||||||
end
|
end
|
||||||
|
|
||||||
def destination
|
# El hostname por defecto incluye WWW
|
||||||
File.join(Rails.root, '_deploy', fqdn)
|
#
|
||||||
end
|
# @return [String]
|
||||||
|
def default_hostname
|
||||||
def fqdn
|
"www.#{site.deploy_local.hostname}"
|
||||||
"www.#{site.hostname}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# Elimina el link simbólico si se elimina este deploy.
|
||||||
def remove_destination!
|
def remove_destination!
|
||||||
FileUtils.rm_f destination
|
FileUtils.rm_f destination
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Obtiene la ubicación relativa del deploy local hacia la ubicación de
|
||||||
|
# este deploy
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def relative_path
|
||||||
|
Pathname.new(site.deploy_local.destination).relative_path_from(File.dirname(destination)).to_s
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,22 +2,24 @@
|
||||||
|
|
||||||
require 'zip'
|
require 'zip'
|
||||||
|
|
||||||
# Genera un ZIP a partir del sitio ya construido
|
# Genera un ZIP a partir del sitio ya generado y lo coloca para descarga
|
||||||
|
# dentro del sitio público.
|
||||||
#
|
#
|
||||||
# TODO: Firmar con minisign
|
# TODO: Firmar con minisign
|
||||||
class DeployZip < Deploy
|
class DeployZip < Deploy
|
||||||
store :values, accessors: %i[], coder: JSON
|
# El hostname es el nombre del archivo.
|
||||||
|
validates :hostname, format: { with: /\.zip\z/ }
|
||||||
|
|
||||||
# Una vez que el sitio está generado, tomar todos los archivos y
|
# Una vez que el sitio está generado, tomar todos los archivos y
|
||||||
# y generar un zip accesible públicamente.
|
# y generar un ZIP accesible públicamente.
|
||||||
#
|
#
|
||||||
# rubocop:disable Metrics/MethodLength
|
# @return [Boolean]
|
||||||
def deploy
|
def deploy
|
||||||
FileUtils.rm_f path
|
remove_destination!
|
||||||
|
|
||||||
time_start
|
time_start
|
||||||
Dir.chdir(destination) do
|
Dir.chdir(destination) do
|
||||||
Zip::File.open(path, Zip::File::CREATE) do |z|
|
Zip::File.open(hostname, Zip::File::CREATE) do |z|
|
||||||
Dir.glob('./**/**').each do |f|
|
Dir.glob('./**/**').each do |f|
|
||||||
File.directory?(f) ? z.mkdir(f) : z.add(f, f)
|
File.directory?(f) ? z.mkdir(f) : z.add(f, f)
|
||||||
end
|
end
|
||||||
|
@ -31,25 +33,47 @@ class DeployZip < Deploy
|
||||||
|
|
||||||
File.exist? path
|
File.exist? path
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/MethodLength
|
|
||||||
|
|
||||||
def limit
|
# La URL de descarga del archivo.
|
||||||
1
|
#
|
||||||
|
# @return [String]
|
||||||
|
def url
|
||||||
|
"#{site.deploy_local.url}/#{hostname}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Devuelve el tamaño del ZIP en bytes
|
||||||
|
#
|
||||||
|
# @return [Integer]
|
||||||
def size
|
def size
|
||||||
File.size path
|
File.size path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# El archivo ZIP se guarda dentro del sitio local para poder
|
||||||
|
# descargarlo luego.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
def destination
|
def destination
|
||||||
File.join(Rails.root, '_deploy', site.hostname)
|
site.deploy_local.destination
|
||||||
end
|
end
|
||||||
|
|
||||||
def file
|
# El "hostname" es la ubicación del archivo.
|
||||||
"#{site.hostname}.zip"
|
#
|
||||||
|
# @return [String]
|
||||||
|
def default_hostname
|
||||||
|
"#{site.deploy_local.hostname}.zip"
|
||||||
end
|
end
|
||||||
|
|
||||||
def path
|
def path
|
||||||
File.join(destination, file)
|
File.join(destination, hostname)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def remove_destination!
|
||||||
|
FileUtils.rm_f path
|
||||||
|
end
|
||||||
|
|
||||||
|
def implements_hostname_validation?
|
||||||
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,7 @@ class Site < ApplicationRecord
|
||||||
include Site::Forms
|
include Site::Forms
|
||||||
include Site::FindAndReplace
|
include Site::FindAndReplace
|
||||||
include Site::Api
|
include Site::Api
|
||||||
|
include Site::Deployment
|
||||||
include Tienda
|
include Tienda
|
||||||
|
|
||||||
# Cifrar la llave privada que cifra y decifra campos ocultos. Sutty
|
# Cifrar la llave privada que cifra y decifra campos ocultos. Sutty
|
||||||
|
@ -15,19 +16,11 @@ class Site < ApplicationRecord
|
||||||
# protege de acceso al panel de Sutty!
|
# protege de acceso al panel de Sutty!
|
||||||
encrypts :private_key
|
encrypts :private_key
|
||||||
|
|
||||||
# TODO: Hacer que los diferentes tipos de deploy se auto registren
|
|
||||||
# @see app/services/site_service.rb
|
|
||||||
DEPLOYS = %i[local private www zip hidden_service].freeze
|
|
||||||
|
|
||||||
validates :name, uniqueness: true, hostname: {
|
|
||||||
allow_root_label: true
|
|
||||||
}
|
|
||||||
|
|
||||||
validates :design_id, presence: true
|
validates :design_id, presence: true
|
||||||
|
validates_uniqueness_of :name
|
||||||
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: 50..160 }
|
||||||
validate :deploy_local_presence
|
|
||||||
validate :compatible_layouts, on: :update
|
validate :compatible_layouts, on: :update
|
||||||
|
|
||||||
attr_reader :incompatible_layouts
|
attr_reader :incompatible_layouts
|
||||||
|
@ -38,8 +31,6 @@ class Site < ApplicationRecord
|
||||||
belongs_to :licencia
|
belongs_to :licencia
|
||||||
|
|
||||||
has_many :log_entries, dependent: :destroy
|
has_many :log_entries, dependent: :destroy
|
||||||
has_many :deploys, dependent: :destroy
|
|
||||||
has_many :build_stats, through: :deploys
|
|
||||||
has_many :roles, dependent: :destroy
|
has_many :roles, dependent: :destroy
|
||||||
has_many :usuaries, -> { where('roles.rol = ?', 'usuarie') },
|
has_many :usuaries, -> { where('roles.rol = ?', 'usuarie') },
|
||||||
through: :roles
|
through: :roles
|
||||||
|
@ -58,13 +49,11 @@ class Site < ApplicationRecord
|
||||||
after_initialize :load_jekyll
|
after_initialize :load_jekyll
|
||||||
after_create :load_jekyll, :static_file_migration!
|
after_create :load_jekyll, :static_file_migration!
|
||||||
# Cambiar el nombre del directorio
|
# Cambiar el nombre del directorio
|
||||||
before_update :update_name!
|
before_update :update_name!, if: :name_changed?
|
||||||
before_save :add_private_key_if_missing!
|
before_save :add_private_key_if_missing!
|
||||||
# Guardar la configuración si hubo cambios
|
# Guardar la configuración si hubo cambios
|
||||||
after_save :sync_attributes_with_config!
|
after_save :sync_attributes_with_config!
|
||||||
|
|
||||||
accepts_nested_attributes_for :deploys, allow_destroy: true
|
|
||||||
|
|
||||||
# El sitio en Jekyll
|
# El sitio en Jekyll
|
||||||
attr_reader :jekyll
|
attr_reader :jekyll
|
||||||
|
|
||||||
|
@ -85,49 +74,6 @@ class Site < ApplicationRecord
|
||||||
@repository ||= Site::Repository.new path
|
@repository ||= Site::Repository.new path
|
||||||
end
|
end
|
||||||
|
|
||||||
def hostname
|
|
||||||
sub = name || I18n.t('deploys.deploy_local.ejemplo')
|
|
||||||
|
|
||||||
if sub.ends_with? '.'
|
|
||||||
sub.gsub(/\.\Z/, '')
|
|
||||||
else
|
|
||||||
"#{sub}.#{Site.domain}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Devuelve la URL siempre actualizada a través del hostname
|
|
||||||
#
|
|
||||||
# @param slash Boolean Agregar / al final o no
|
|
||||||
# @return String La URL con o sin / al final
|
|
||||||
def url(slash: true)
|
|
||||||
"https://#{hostname}#{slash ? '/' : ''}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Obtiene los dominios alternativos
|
|
||||||
#
|
|
||||||
# @return Array
|
|
||||||
def alternative_hostnames
|
|
||||||
deploys.where(type: 'DeployAlternativeDomain').map(&:hostname).map do |h|
|
|
||||||
h.end_with?('.') ? h[0..-2] : "#{h}.#{Site.domain}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Obtiene todas las URLs alternativas para este sitio
|
|
||||||
#
|
|
||||||
# @return Array
|
|
||||||
def alternative_urls(slash: true)
|
|
||||||
alternative_hostnames.map do |h|
|
|
||||||
"https://#{h}#{slash ? '/' : ''}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Todas las URLs posibles para este sitio
|
|
||||||
#
|
|
||||||
# @return Array
|
|
||||||
def urls(slash: true)
|
|
||||||
alternative_urls(slash: slash) << url(slash: slash)
|
|
||||||
end
|
|
||||||
|
|
||||||
def invitade?(usuarie)
|
def invitade?(usuarie)
|
||||||
!invitades.find_by(id: usuarie.id).nil?
|
!invitades.find_by(id: usuarie.id).nil?
|
||||||
end
|
end
|
||||||
|
@ -453,8 +399,6 @@ class Site < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_name!
|
def update_name!
|
||||||
return unless name_changed?
|
|
||||||
|
|
||||||
FileUtils.mv path_was, path
|
FileUtils.mv path_was, path
|
||||||
reload_jekyll!
|
reload_jekyll!
|
||||||
end
|
end
|
||||||
|
@ -477,19 +421,6 @@ class Site < ApplicationRecord
|
||||||
Site::StaticFileMigration.new(site: self).migrate!
|
Site::StaticFileMigration.new(site: self).migrate!
|
||||||
end
|
end
|
||||||
|
|
||||||
# Valida si el sitio tiene al menos una forma de alojamiento asociada
|
|
||||||
# y es la local
|
|
||||||
#
|
|
||||||
# TODO: Volver opcional el alojamiento local, pero ahora mismo está
|
|
||||||
# atado a la generación del sitio así que no puede faltar
|
|
||||||
def deploy_local_presence
|
|
||||||
# Usamos size porque queremos saber la cantidad de deploys sin
|
|
||||||
# guardar también
|
|
||||||
return if deploys.size.positive? && deploys.map(&:type).include?('DeployLocal')
|
|
||||||
|
|
||||||
errors.add(:deploys, I18n.t('activerecord.errors.models.site.attributes.deploys.deploy_local_presence'))
|
|
||||||
end
|
|
||||||
|
|
||||||
# Valida que al cambiar de plantilla no tengamos artículos en layouts
|
# Valida que al cambiar de plantilla no tengamos artículos en layouts
|
||||||
# inexistentes.
|
# inexistentes.
|
||||||
def compatible_layouts
|
def compatible_layouts
|
||||||
|
|
113
app/models/site/deployment.rb
Normal file
113
app/models/site/deployment.rb
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Site
|
||||||
|
# Abstrae todo el comportamiento de publicación del sitio en un
|
||||||
|
# módulo.
|
||||||
|
module Deployment
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
# TODO: Hacer que los diferentes tipos de deploy se auto registren
|
||||||
|
# @see app/services/site_service.rb
|
||||||
|
DEPLOYS = %i[local private www zip hidden_service].freeze
|
||||||
|
|
||||||
|
validates :name,
|
||||||
|
format: { with: /\A[0-9a-z\-]+\z/,
|
||||||
|
message: I18n.t('activerecord.errors.models.site.attributes.name.no_subdomains') }
|
||||||
|
validates :name, hostname: true
|
||||||
|
validates_presence_of :canonical_deploy
|
||||||
|
validate :deploy_local_presence
|
||||||
|
validate :name_changed_is_unique_hostname, if: :name_changed?
|
||||||
|
|
||||||
|
has_one :canonical_deploy, class_name: 'Deploy'
|
||||||
|
has_many :deploys, dependent: :destroy
|
||||||
|
has_many :access_logs, through: :deploys
|
||||||
|
has_many :build_stats, through: :deploys
|
||||||
|
|
||||||
|
before_validation :deploy_local_is_default_canonical_deploy!, unless: :canonical_deploy_id?
|
||||||
|
before_update :update_deploy_local_hostname!, if: :name_changed?
|
||||||
|
|
||||||
|
accepts_nested_attributes_for :deploys, allow_destroy: true
|
||||||
|
|
||||||
|
# El primer deploy del sitio, si no existe en la base de datos es
|
||||||
|
# porque recién estamos creando el sitio y todavía no se guardó.
|
||||||
|
#
|
||||||
|
# @return [DeployLocal]
|
||||||
|
def deploy_local
|
||||||
|
@deploy_local ||= deploys.order(created_at: :asc).find_by(type: 'DeployLocal') || deploys.find do |d|
|
||||||
|
d.type == 'DeployLocal'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Obtiene la URL principal
|
||||||
|
#
|
||||||
|
# @param :slash [Boolean]
|
||||||
|
# @return [String]
|
||||||
|
def canonical_url(slash: true)
|
||||||
|
canonical_deploy.url.dup.tap do |url|
|
||||||
|
url << '/' if slash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
alias_method :url, :canonical_url
|
||||||
|
|
||||||
|
# Devuelve todas las URLs posibles
|
||||||
|
#
|
||||||
|
# @param :slash [Boolean]
|
||||||
|
# @return [Array]
|
||||||
|
def urls(slash: true)
|
||||||
|
deploys.map(&:url).map do |url|
|
||||||
|
slash ? "#{url}/" : url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Obtiene el hostname principal
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def hostname
|
||||||
|
canonical_deploy.hostname
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Validar que al cambiar el nombre no estemos utilizando un
|
||||||
|
# hostname reservado por otro sitio.
|
||||||
|
#
|
||||||
|
# Al cambiar el nombre del DeployLocal se va a validar que el
|
||||||
|
# hostname nuevo sea único.
|
||||||
|
def name_changed_is_unique_hostname
|
||||||
|
deploy_local.hostname = nil
|
||||||
|
|
||||||
|
return if deploy_local.valid?
|
||||||
|
|
||||||
|
errors.add :name, I18n.t('activerecord.errors.models.site.attributes.name.duplicated_hostname')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Si cambia el nombre queremos actualizarlo en el DeployLocal y
|
||||||
|
# recargar el deploy canónico para tomar el nombre que
|
||||||
|
# corresponda.
|
||||||
|
def update_deploy_local_hostname!
|
||||||
|
deploy_local.update(hostname: name)
|
||||||
|
canonical_deploy.reload if canonical_deploy == deploy_local
|
||||||
|
end
|
||||||
|
|
||||||
|
# Si no asignamos un deploy canónico en el momento le asignamos el
|
||||||
|
# deploy local
|
||||||
|
def deploy_local_is_default_canonical_deploy!
|
||||||
|
self.canonical_deploy ||= deploy_local
|
||||||
|
end
|
||||||
|
|
||||||
|
# Valida si el sitio tiene al menos una forma de alojamiento asociada
|
||||||
|
# y es la local
|
||||||
|
#
|
||||||
|
# TODO: Volver opcional el alojamiento local, pero ahora mismo está
|
||||||
|
# atado a la generación del sitio así que no puede faltar
|
||||||
|
def deploy_local_presence
|
||||||
|
# Usamos size porque queremos saber la cantidad de deploys sin
|
||||||
|
# guardar también
|
||||||
|
return if deploys.size.positive? && deploys.map(&:type).include?('DeployLocal')
|
||||||
|
|
||||||
|
errors.add(:deploys, I18n.t('activerecord.errors.models.site.attributes.deploys.deploy_local_presence'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -13,11 +13,10 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do
|
I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do
|
||||||
site.save &&
|
site.save &&
|
||||||
site.config.write &&
|
site.config.write &&
|
||||||
commit_config(action: :create)
|
commit_config(action: :create) &&
|
||||||
|
add_licencias
|
||||||
end
|
end
|
||||||
|
|
||||||
add_licencias
|
|
||||||
|
|
||||||
site
|
site
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
%h1= t('.hi')
|
%h1= t('.hi')
|
||||||
|
|
||||||
= sanitize_markdown t('.explanation', fqdn: @deploy_local.site.hostname),
|
= sanitize_markdown t('.explanation', url: @site.deploy_local.url),
|
||||||
tags: %w[p a strong em]
|
tags: %w[p a strong em]
|
||||||
|
|
||||||
%table
|
%table
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
= '# ' + t('.hi')
|
= '# ' + t('.hi')
|
||||||
\
|
\
|
||||||
= t('.explanation', fqdn: @deploy_local.site.hostname)
|
= t('.explanation', url: @site.deploy_local.url)
|
||||||
\
|
\
|
||||||
= Terminal::Table.new do |table|
|
= Terminal::Table.new do |table|
|
||||||
- table << [t('.th.type'), t('.th.status')]
|
- table << [t('.th.type'), t('.th.status')]
|
||||||
|
|
|
@ -14,10 +14,10 @@
|
||||||
'0', '1'
|
'0', '1'
|
||||||
= deploy.label :_destroy, class: 'custom-control-label' do
|
= deploy.label :_destroy, class: 'custom-control-label' do
|
||||||
%h3= t('.title')
|
%h3= t('.title')
|
||||||
= sanitize_markdown t('.help', public_url: deploy.object.site.url),
|
= sanitize_markdown t('.help', public_url: site.deploy_local.url),
|
||||||
tags: %w[p strong em a]
|
tags: %w[p strong em a]
|
||||||
|
|
||||||
- if deploy.object.fqdn
|
- unless deploy.object.temporary?
|
||||||
= sanitize_markdown t('.help_2', url: deploy.object.url),
|
= sanitize_markdown t('.help_2', url: deploy.object.url),
|
||||||
tags: %w[p strong em a]
|
tags: %w[p strong em a]
|
||||||
%hr/
|
%hr/
|
||||||
|
|
|
@ -6,7 +6,9 @@
|
||||||
.row
|
.row
|
||||||
.col
|
.col
|
||||||
%h3= t('.title')
|
%h3= t('.title')
|
||||||
= sanitize_markdown t('.help', fqdn: deploy.object.site.hostname),
|
= sanitize_markdown t('.help', url: deploy.object.url),
|
||||||
tags: %w[p strong em a]
|
tags: %w[p strong em a]
|
||||||
|
|
||||||
= deploy.hidden_field :type
|
-# No duplicarlos una vez que existen.
|
||||||
|
- unless deploy.object.persisted?
|
||||||
|
= deploy.hidden_field :type
|
||||||
|
|
|
@ -15,6 +15,6 @@
|
||||||
'0', '1'
|
'0', '1'
|
||||||
= deploy.label :_destroy, class: 'custom-control-label' do
|
= deploy.label :_destroy, class: 'custom-control-label' do
|
||||||
%h3= t('.title')
|
%h3= t('.title')
|
||||||
= sanitize_markdown t('.help', fqdn: deploy.object.fqdn),
|
= sanitize_markdown t('.help', url: deploy.object.url),
|
||||||
tags: %w[p strong em a]
|
tags: %w[p strong em a]
|
||||||
%hr/
|
%hr/
|
||||||
|
|
|
@ -15,10 +15,5 @@
|
||||||
'0', '1'
|
'0', '1'
|
||||||
= deploy.label :_destroy, class: 'custom-control-label' do
|
= deploy.label :_destroy, class: 'custom-control-label' do
|
||||||
%h3= t('.title')
|
%h3= t('.title')
|
||||||
-# TODO: secar la generación de URLs
|
= sanitize_markdown t('.help', url: deploy.object.url), tags: %w[p strong em a]
|
||||||
- name = site.name || t('.ejemplo')
|
|
||||||
= sanitize_markdown t('.help',
|
|
||||||
fqdn: deploy.object.site.hostname,
|
|
||||||
file: deploy.object.file || "#{name}.zip"),
|
|
||||||
tags: %w[p strong em a]
|
|
||||||
%hr/
|
%hr/
|
||||||
|
|
|
@ -13,29 +13,19 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
||||||
# El problema sería que otros sitios con JS malicioso hagan pedidos
|
# El problema sería que otros sitios con JS malicioso hagan pedidos
|
||||||
# a nuestra API desde otros sitios infectados.
|
# a nuestra API desde otros sitios infectados.
|
||||||
#
|
#
|
||||||
# XXX: La primera parte del dominio tiene que coincidir con el
|
|
||||||
# nombre del sitio.
|
|
||||||
#
|
|
||||||
# XXX: Al terminar de entender esto nos pasó que el servidor recibe
|
# XXX: Al terminar de entender esto nos pasó que el servidor recibe
|
||||||
# la petición de todas maneras, con lo que no estamos previniendo
|
# la petición de todas maneras, con lo que no estamos previniendo
|
||||||
# que nos hablen, sino que lean información. Solo va a funcionar si
|
# que nos hablen, sino que lean información. Solo va a funcionar si
|
||||||
# el servidor no tiene el Preflight cacheado.
|
# el servidor no tiene el Preflight cacheado.
|
||||||
#
|
#
|
||||||
# TODO: Limitar el acceso desde Nginx también.
|
# TODO: Limitar el acceso desde Nginx también.
|
||||||
#
|
|
||||||
# TODO: Poder consultar por sitios por todas sus URLs posibles.
|
|
||||||
origins do |source, _|
|
origins do |source, _|
|
||||||
# Cacheamos la respuesta para no tener que volver a procesarla
|
# Cacheamos la respuesta para no tener que volver a procesarla
|
||||||
# cada vez.
|
# cada vez.
|
||||||
Rails.cache.fetch(source, expires_in: 1.hour) do
|
Rails.cache.fetch(source, expires_in: 1.hour) do
|
||||||
uri = URI(source)
|
hostname = URI(source)&.host
|
||||||
|
hostname.present? && Deploy.find_by_hostname(hostname).present?
|
||||||
if (name = uri&.host&.split('.', 2)&.first).present?
|
rescue StandardError
|
||||||
Site.where(name: [name, uri.host + '.']).pluck(:name).first.present?
|
|
||||||
else
|
|
||||||
false
|
|
||||||
end
|
|
||||||
rescue URI::Error
|
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
6
config/initializers/validates_hostname.rb
Normal file
6
config/initializers/validates_hostname.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Agrega el subdominio .local a menos que estemos en producción.
|
||||||
|
#
|
||||||
|
# TODO: Permitir TLDs que no sean de ICANN aquí.
|
||||||
|
PAK::ValidatesHostname::ALLOWED_TLDS << 'local' unless Rails.env.production?
|
|
@ -70,7 +70,7 @@ en:
|
||||||
hi: "Hi!"
|
hi: "Hi!"
|
||||||
explanation: |
|
explanation: |
|
||||||
This e-mail is to notify you that Sutty has built your site, which is
|
This e-mail is to notify you that Sutty has built your site, which is
|
||||||
available at <https://%{fqdn}>.
|
available at <%{url}/>.
|
||||||
|
|
||||||
You'll find details below.
|
You'll find details below.
|
||||||
th:
|
th:
|
||||||
|
@ -143,8 +143,15 @@ en:
|
||||||
tienda_api_key: Store access key
|
tienda_api_key: Store access key
|
||||||
errors:
|
errors:
|
||||||
models:
|
models:
|
||||||
|
deploy:
|
||||||
|
attributes:
|
||||||
|
hostname:
|
||||||
|
destination_exist: 'There already is a file in the destination'
|
||||||
site:
|
site:
|
||||||
attributes:
|
attributes:
|
||||||
|
name:
|
||||||
|
no_subdomains: 'Name cannot contain dots'
|
||||||
|
duplicated_hostname: 'There already is a site with this address'
|
||||||
deploys:
|
deploys:
|
||||||
deploy_local_presence: 'We need to be build the site!'
|
deploy_local_presence: 'We need to be build the site!'
|
||||||
design_id:
|
design_id:
|
||||||
|
@ -195,7 +202,7 @@ en:
|
||||||
deploy_local:
|
deploy_local:
|
||||||
title: 'Host at Sutty'
|
title: 'Host at Sutty'
|
||||||
help: |
|
help: |
|
||||||
The site will be available at <https://%{fqdn}/>.
|
The site will be available at <%{url}/>.
|
||||||
|
|
||||||
We're working out the details to allow you to use your own site
|
We're working out the details to allow you to use your own site
|
||||||
domains, you can [help us](https://sutty.nl/en/index.html#contact)!
|
domains, you can [help us](https://sutty.nl/en/index.html#contact)!
|
||||||
|
@ -211,7 +218,7 @@ en:
|
||||||
title: 'Add www to the address'
|
title: 'Add www to the address'
|
||||||
help: |
|
help: |
|
||||||
When you enable this option, your site will also be available
|
When you enable this option, your site will also be available
|
||||||
under <https://%{fqdn}/>.
|
under <%{url}/>.
|
||||||
|
|
||||||
The www prefix has been a way of referring to
|
The www prefix has been a way of referring to
|
||||||
computers that are available on the World Wide Web. Since
|
computers that are available on the World Wide Web. Since
|
||||||
|
@ -222,7 +229,7 @@ en:
|
||||||
help: |
|
help: |
|
||||||
ZIP files contain and compress all your site's files. With
|
ZIP files contain and compress all your site's files. With
|
||||||
this option you can download and also share your entire site
|
this option you can download and also share your entire site
|
||||||
through the <https://%{fqdn}/%{file}> address, keep it as backup
|
through the <%{url}> address, keep it as backup
|
||||||
or have a strategy of solidary hosting, where many people
|
or have a strategy of solidary hosting, where many people
|
||||||
share a copy of your site.
|
share a copy of your site.
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ es:
|
||||||
hi: "¡Hola!"
|
hi: "¡Hola!"
|
||||||
explanation: |
|
explanation: |
|
||||||
Este correo es para notificarte que Sutty ha generado tu sitio y
|
Este correo es para notificarte que Sutty ha generado tu sitio y
|
||||||
ya está disponible en la dirección <https://%{fqdn}>.
|
ya está disponible en la dirección <%{url}>.
|
||||||
|
|
||||||
A continuación encontrarás el detalle de lo que hicimos.
|
A continuación encontrarás el detalle de lo que hicimos.
|
||||||
th:
|
th:
|
||||||
|
@ -143,8 +143,15 @@ es:
|
||||||
tienda_api_key: Clave de acceso
|
tienda_api_key: Clave de acceso
|
||||||
errors:
|
errors:
|
||||||
models:
|
models:
|
||||||
|
deploy:
|
||||||
|
attributes:
|
||||||
|
hostname:
|
||||||
|
destination_exist: 'Ya hay un archivo en esta ubicación'
|
||||||
site:
|
site:
|
||||||
attributes:
|
attributes:
|
||||||
|
name:
|
||||||
|
no_subdomains: 'El nombre no puede contener puntos'
|
||||||
|
duplicated_hostname: 'Ya existe un sitio con ese nombre'
|
||||||
deploys:
|
deploys:
|
||||||
deploy_local_presence: '¡Necesitamos poder generar el sitio!'
|
deploy_local_presence: '¡Necesitamos poder generar el sitio!'
|
||||||
design_id:
|
design_id:
|
||||||
|
@ -197,7 +204,7 @@ es:
|
||||||
deploy_local:
|
deploy_local:
|
||||||
title: 'Alojar en Sutty'
|
title: 'Alojar en Sutty'
|
||||||
help: |
|
help: |
|
||||||
El sitio estará disponible en <https://%{fqdn}/>.
|
El sitio estará disponible en <%{url}/>.
|
||||||
|
|
||||||
Estamos desarrollando la posibilidad de agregar tus propios
|
Estamos desarrollando la posibilidad de agregar tus propios
|
||||||
dominios, ¡ayudanos!
|
dominios, ¡ayudanos!
|
||||||
|
@ -213,7 +220,7 @@ es:
|
||||||
title: 'Agregar www a la dirección'
|
title: 'Agregar www a la dirección'
|
||||||
help: |
|
help: |
|
||||||
Cuando habilitas esta opción, tu sitio también estará disponible
|
Cuando habilitas esta opción, tu sitio también estará disponible
|
||||||
como <https://%{fqdn}/>.
|
como <%{url}/>.
|
||||||
|
|
||||||
El prefijo www para las direcciones web ha sido una forma de
|
El prefijo www para las direcciones web ha sido una forma de
|
||||||
referirse a las computadoras que están disponibles en la _World
|
referirse a las computadoras que están disponibles en la _World
|
||||||
|
@ -226,7 +233,7 @@ es:
|
||||||
help: |
|
help: |
|
||||||
Los archivos ZIP contienen y comprimen todos los archivos de tu
|
Los archivos ZIP contienen y comprimen todos los archivos de tu
|
||||||
sitio. Con esta opción podrás descargar y compartir tu sitio
|
sitio. Con esta opción podrás descargar y compartir tu sitio
|
||||||
entero a través de la dirección <https://%{fqdn}/%{file}> y
|
entero a través de la dirección <%{url}> y
|
||||||
guardarla como copia de seguridad o una estrategia de
|
guardarla como copia de seguridad o una estrategia de
|
||||||
alojamiento solidario, donde muchas personas comparten una copia
|
alojamiento solidario, donde muchas personas comparten una copia
|
||||||
de tu sitio.
|
de tu sitio.
|
||||||
|
|
56
db/migrate/20210801060844_add_hostname_to_deploys.rb
Normal file
56
db/migrate/20210801060844_add_hostname_to_deploys.rb
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Recupera la funcionalidad que estamos deprecando.
|
||||||
|
module AddValuesToDeploy
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
store :values, accessors: %i[hostname onion], coder: JSON
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Convertir todos los valores serializados de Deploy en una columna,
|
||||||
|
# porque al final el único uso que tuvo fue para guardar los hostnames
|
||||||
|
# alternativos.
|
||||||
|
#
|
||||||
|
# ¡El hostname es único para poder evitar que haya duplicados!
|
||||||
|
class AddHostnameToDeploys < ActiveRecord::Migration[6.1]
|
||||||
|
# Crea una columna temporal y guarda todos los valores. Los traspasa
|
||||||
|
# y luego elimina la columna.
|
||||||
|
def up
|
||||||
|
Deploy.include AddValuesToDeploy
|
||||||
|
# Ya que estamos hacer limpieza.
|
||||||
|
Deploy.where(site_id: nil).destroy_all
|
||||||
|
|
||||||
|
add_column :deploys, :hostname_tmp, :string
|
||||||
|
|
||||||
|
Site.find_each do |site|
|
||||||
|
site.deploys.find_each do |deploy|
|
||||||
|
deploy.hostname_tmp = deploy.values[:hostname] || deploy.values[:onion] || deploy.hostname
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
rename_column :deploys, :hostname_tmp, :hostname
|
||||||
|
remove_column :deploys, :values
|
||||||
|
|
||||||
|
add_index :deploys, :hostname, unique: true
|
||||||
|
# A esta altura todos los dominios deberían estar migrados.
|
||||||
|
change_column :deploys, :hostname, :string, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Recupera los valores desde la columna creada.
|
||||||
|
def down
|
||||||
|
Deploy.include AddValuesToDeploy
|
||||||
|
|
||||||
|
rename_column :deploys, :hostname, :hostname_tmp
|
||||||
|
add_column :deploys, :values, :text
|
||||||
|
|
||||||
|
Site.find_each do |site|
|
||||||
|
site.deploys.find_each do |deploy|
|
||||||
|
deploy.values[(deploy.is_a? DeployHiddenService ? :onion : :hostname)] = deploy.hostname_tmp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
remove_column :deploys, :hostname_tmp
|
||||||
|
end
|
||||||
|
end
|
26
db/migrate/20210809155434_add_canonical_deploy_to_sites.rb
Normal file
26
db/migrate/20210809155434_add_canonical_deploy_to_sites.rb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Los sitios pueden tener muchos tipos de publicación pero solo uno es
|
||||||
|
# el principal. Al usar un campo específico, podemos validar mejor su
|
||||||
|
# presencia y modificación.
|
||||||
|
#
|
||||||
|
# El valor por defecto es 0 para poder crear la columna sin modificarla
|
||||||
|
# después, pero la idea es que nunca haya ceros.
|
||||||
|
class AddCanonicalDeployToSites < ActiveRecord::Migration[6.1]
|
||||||
|
def up
|
||||||
|
add_belongs_to :sites, :canonical_deploy, index: true, null: false, default: 0
|
||||||
|
|
||||||
|
# Si el sitio tenía un dominio alternativo, usar ese en lugar del
|
||||||
|
# local, asumiendo que es el primero de todos los posibles.
|
||||||
|
Site.find_each do |site|
|
||||||
|
deploy = site.deploys.order(created_at: :asc).find_by_type('DeployAlternativeDomain')
|
||||||
|
deploy ||= site.deploy_local
|
||||||
|
|
||||||
|
site.update canonical_deploy_id: deploy.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_belongs_to :sites, :canonical_deploy, index: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,13 +21,14 @@ module Api
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'el sitio tiene que existir' do
|
test 'el sitio tiene que existir' do
|
||||||
|
hostname = @site.hostname
|
||||||
@site.destroy
|
@site.destroy
|
||||||
|
|
||||||
get v1_site_contact_cookie_url(@site.hostname, **@host)
|
get v1_site_contact_cookie_url(hostname, **@host)
|
||||||
|
|
||||||
assert_not cookies[@site.name]
|
assert_not cookies[@site.name]
|
||||||
|
|
||||||
post v1_site_contact_url(site_id: @site.hostname, form: :contacto, **@host),
|
post v1_site_contact_url(site_id: hostname, form: :contacto, **@host),
|
||||||
params: {
|
params: {
|
||||||
name: SecureRandom.hex,
|
name: SecureRandom.hex,
|
||||||
pronouns: SecureRandom.hex,
|
pronouns: SecureRandom.hex,
|
||||||
|
@ -106,7 +107,7 @@ module Api
|
||||||
test 'se puede enviar mensajes a dominios propios' do
|
test 'se puede enviar mensajes a dominios propios' do
|
||||||
ActionMailer::Base.deliveries.clear
|
ActionMailer::Base.deliveries.clear
|
||||||
|
|
||||||
@site.update name: 'example.org.'
|
@site.update name: 'example'
|
||||||
|
|
||||||
redirect = "#{@site.url}?thanks"
|
redirect = "#{@site.url}?thanks"
|
||||||
|
|
||||||
|
@ -130,6 +131,34 @@ module Api
|
||||||
assert_equal redirect, response.headers['Location']
|
assert_equal redirect, response.headers['Location']
|
||||||
assert_equal 2, ActionMailer::Base.deliveries.size
|
assert_equal 2, ActionMailer::Base.deliveries.size
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test 'algunos navegadores no soportan Origin' do
|
||||||
|
ActionMailer::Base.deliveries.clear
|
||||||
|
|
||||||
|
@site.update name: 'example'
|
||||||
|
|
||||||
|
redirect = "#{@site.url}?thanks"
|
||||||
|
|
||||||
|
10.times do
|
||||||
|
create :rol, site: @site
|
||||||
|
end
|
||||||
|
|
||||||
|
get v1_site_contact_cookie_url(@site.hostname, **@host)
|
||||||
|
post v1_site_contact_url(site_id: @site.hostname, form: :contacto, **@host),
|
||||||
|
headers: { referer: @site.url },
|
||||||
|
params: {
|
||||||
|
name: SecureRandom.hex,
|
||||||
|
pronouns: SecureRandom.hex,
|
||||||
|
contact: SecureRandom.hex,
|
||||||
|
from: "#{SecureRandom.hex}@sutty.nl",
|
||||||
|
body: SecureRandom.hex,
|
||||||
|
consent: true,
|
||||||
|
redirect: redirect
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal redirect, response.headers['Location']
|
||||||
|
assert_equal 2, ActionMailer::Base.deliveries.size
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,7 +23,7 @@ module Api
|
||||||
|
|
||||||
test 'se puede obtener un listado de todos' do
|
test 'se puede obtener un listado de todos' do
|
||||||
get v1_sites_url(host: "api.#{Site.domain}"), headers: @authorization, as: :json
|
get v1_sites_url(host: "api.#{Site.domain}"), headers: @authorization, as: :json
|
||||||
assert_equal Site.all.pluck(:name), JSON.parse(response.body)
|
assert_equal Deploy.all.pluck(:hostname), JSON.parse(response.body)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -119,12 +119,7 @@ class SitesControllerTest < ActionDispatch::IntegrationTest
|
||||||
title: name,
|
title: name,
|
||||||
description: name * 2,
|
description: name * 2,
|
||||||
design_id: design.id,
|
design_id: design.id,
|
||||||
licencia_id: Licencia.all.second.id,
|
licencia_id: Licencia.all.second.id
|
||||||
deploys_attributes: {
|
|
||||||
'0' => {
|
|
||||||
type: 'DeployLocal'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,11 @@ FactoryBot.define do
|
||||||
licencia
|
licencia
|
||||||
|
|
||||||
after :build do |site|
|
after :build do |site|
|
||||||
site.deploys << build(:deploy_local, site: site)
|
# XXX: Generamos un DeployLocal normalmente y no a través de una
|
||||||
end
|
# Factory porque necesitamos que el sitio se genere solo.
|
||||||
|
#
|
||||||
after :create do |site|
|
# @see {https://github.com/thoughtbot/factory_bot/wiki/How-factory_bot-interacts-with-ActiveRecord}
|
||||||
site.deploys << create(:deploy_local, site: site)
|
site.deploys.build(type: 'DeployLocal')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,11 +2,7 @@
|
||||||
|
|
||||||
class DeployJobTest < ActiveSupport::TestCase
|
class DeployJobTest < ActiveSupport::TestCase
|
||||||
test 'se puede compilar' do
|
test 'se puede compilar' do
|
||||||
rol = create :rol
|
site = create :site
|
||||||
site = rol.site
|
|
||||||
site.deploys << create(:deploy_zip, site: site)
|
|
||||||
|
|
||||||
site.save
|
|
||||||
|
|
||||||
DeployJob.perform_async(site.id)
|
DeployJob.perform_async(site.id)
|
||||||
|
|
||||||
|
|
44
test/models/deploy_alternative_domain_test.rb
Normal file
44
test/models/deploy_alternative_domain_test.rb
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class DeployAlternativeDomainTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@site = create :site
|
||||||
|
@deploy_alt = @site.deploys.build type: 'DeployAlternativeDomain'
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
@site&.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_tld
|
||||||
|
PAK::ValidatesHostname::ALLOWED_TLDS.sample
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'el hostname se ingresa manualmente' do
|
||||||
|
assert_nil @deploy_alt.hostname
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'el hostname es obligatorio' do
|
||||||
|
assert_not @deploy_alt.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'el hostname es válido' do
|
||||||
|
assert_not @deploy_alt.update(hostname: ' ')
|
||||||
|
assert_not @deploy_alt.update(hostname: 'custom.domain.root.')
|
||||||
|
assert_not @deploy_alt.update(hostname: 'custom.domain')
|
||||||
|
assert @deploy_alt.update(hostname: "custom.domain.#{random_tld}")
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'el hostname tiene que ser único' do
|
||||||
|
assert_not @deploy_alt.update(hostname: @site.hostname)
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'se puede deployear' do
|
||||||
|
assert @site.deploy_local.deploy
|
||||||
|
assert @deploy_alt.update(hostname: "#{SecureRandom.hex}.sutty.#{random_tld}")
|
||||||
|
assert @deploy_alt.deploy
|
||||||
|
assert File.symlink?(@deploy_alt.destination)
|
||||||
|
end
|
||||||
|
end
|
36
test/models/deploy_hidden_service_test.rb
Normal file
36
test/models/deploy_hidden_service_test.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class DeployHiddenServiceTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@site = create :site
|
||||||
|
@deploy_hidden = @site.deploys.build type: 'DeployHiddenService'
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
@site&.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'el hostname es válido' do
|
||||||
|
assert_not @deploy_hidden.update(hostname: ' ')
|
||||||
|
assert_not @deploy_hidden.update(hostname: 'custom.domain.root.')
|
||||||
|
assert_not @deploy_hidden.update(hostname: 'custom.domain')
|
||||||
|
assert @deploy_hidden.update(hostname: "#{@deploy_hidden.send(:random_base32, 56)}.onion")
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'los hostnames pueden ser temporales' do
|
||||||
|
assert @deploy_hidden.hostname.start_with? 'temporary'
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'el hostname tiene que ser único' do
|
||||||
|
assert @deploy_hidden.save
|
||||||
|
assert_not @site.deploys.create(type: 'DeployHiddenService', hostname: @deploy_hidden.hostname).valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'se puede deployear' do
|
||||||
|
assert @site.deploy_local.deploy
|
||||||
|
assert @deploy_hidden.deploy
|
||||||
|
assert File.symlink?(@deploy_hidden.destination)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,24 +1,44 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
class DeployLocalTest < ActiveSupport::TestCase
|
class DeployLocalTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@site = create :site
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
@site&.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'se pueden crear' do
|
||||||
|
assert @site.deploy_local.valid?
|
||||||
|
assert_equal @site.hostname, @site.deploy_local.hostname
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'no se puede cambiar el hostname' do
|
||||||
|
hostname = @site.deploy_local.hostname
|
||||||
|
@site.deploy_local.hostname = SecureRandom.hex
|
||||||
|
|
||||||
|
assert @site.deploy_local.save
|
||||||
|
assert_equal hostname, @site.deploy_local.hostname
|
||||||
|
end
|
||||||
|
|
||||||
test 'se puede deployear' do
|
test 'se puede deployear' do
|
||||||
site = create :site
|
deploy_local = @site.deploy_local
|
||||||
local = create :deploy_local, site: site
|
|
||||||
deploy = create :deploy_zip, site: site
|
|
||||||
|
|
||||||
# Primero tenemos que generar el sitio
|
assert deploy_local.deploy
|
||||||
local.deploy
|
assert File.directory?(deploy_local.destination)
|
||||||
|
assert File.exist?(File.join(deploy_local.destination, 'index.html'))
|
||||||
|
assert_equal 3, deploy_local.build_stats.count
|
||||||
|
|
||||||
escaped_path = Shellwords.escape(deploy.path)
|
assert deploy_local.build_stats.map(&:bytes).compact.inject(:+).positive?
|
||||||
|
assert deploy_local.build_stats.map(&:seconds).compact.inject(:+).positive?
|
||||||
|
end
|
||||||
|
|
||||||
assert deploy.deploy
|
test 'al eliminarlos se elimina el directorio' do
|
||||||
assert File.file?(deploy.path)
|
deploy_local = @site.deploy_local
|
||||||
assert_equal 'application/zip',
|
assert deploy_local.destroy
|
||||||
`file --mime-type "#{escaped_path}"`.split(' ').last
|
assert_not File.directory?(deploy_local.destination)
|
||||||
assert_equal 1, deploy.build_stats.count
|
|
||||||
assert deploy.build_stats.map(&:bytes).inject(:+).positive?
|
|
||||||
assert deploy.build_stats.map(&:seconds).inject(:+).positive?
|
|
||||||
|
|
||||||
local.destroy
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,17 +3,23 @@
|
||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
|
|
||||||
class DeployWwwTest < ActiveSupport::TestCase
|
class DeployWwwTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@site = create :site
|
||||||
|
@deploy_www = @site.deploys.create type: 'DeployWww'
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
@site&.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'el hostname empieza con www' do
|
||||||
|
assert @deploy_www.hostname.start_with?('www.')
|
||||||
|
end
|
||||||
|
|
||||||
test 'se puede deployear' do
|
test 'se puede deployear' do
|
||||||
site = create :site
|
assert @site.deploy_local.deploy
|
||||||
local = create :deploy_local, site: site
|
|
||||||
deploy = create :deploy_www, site: site
|
|
||||||
|
|
||||||
# Primero tenemos que generar el sitio
|
assert @deploy_www.deploy
|
||||||
local.deploy
|
assert File.symlink?(@deploy_www.destination)
|
||||||
|
|
||||||
assert deploy.deploy
|
|
||||||
assert File.symlink?(deploy.destination)
|
|
||||||
|
|
||||||
local.destroy
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,18 +3,29 @@
|
||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
|
|
||||||
class DeployZipTest < ActiveSupport::TestCase
|
class DeployZipTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@site = create :site
|
||||||
|
@deploy_zip = @site.deploys.create(type: 'DeployZip')
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
@site&.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'el nombre es el hostname.zip' do
|
||||||
|
assert_equal "#{@site.hostname}.zip", @deploy_zip.hostname
|
||||||
|
end
|
||||||
|
|
||||||
test 'se puede deployear' do
|
test 'se puede deployear' do
|
||||||
deploy_local = create :deploy_local
|
# Primero tenemos que generar el sitio
|
||||||
|
assert @site.deploy_local.deploy
|
||||||
|
|
||||||
assert deploy_local.deploy
|
assert @deploy_zip.deploy
|
||||||
assert File.directory?(deploy_local.destination)
|
assert File.file?(@deploy_zip.path)
|
||||||
assert File.exist?(File.join(deploy_local.destination, 'index.html'))
|
assert_equal 'application/zip',
|
||||||
assert_equal 3, deploy_local.build_stats.count
|
`file --mime-type "#{@deploy_zip.path}"`.split.last
|
||||||
|
assert_equal 1, @deploy_zip.build_stats.count
|
||||||
assert deploy_local.build_stats.map(&:bytes).compact.inject(:+).positive?
|
assert @deploy_zip.build_stats.map(&:bytes).inject(:+).positive?
|
||||||
assert deploy_local.build_stats.map(&:seconds).compact.inject(:+).positive?
|
assert @deploy_zip.build_stats.map(&:seconds).inject(:+).positive?
|
||||||
|
|
||||||
assert deploy_local.destroy
|
|
||||||
assert_not File.directory?(deploy_local.destination)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
88
test/models/site/deployment_test.rb
Normal file
88
test/models/site/deployment_test.rb
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class Site::DeploymentTest < ActiveSupport::TestCase
|
||||||
|
def site
|
||||||
|
@site ||= create :site
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
@site&.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'al publicar el sitio se crea el directorio' do
|
||||||
|
assert site.deploy_local.deploy
|
||||||
|
assert site.deploy_local.exist?
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'al cambiar el nombre no puede pisar un dominio que ya existe' do
|
||||||
|
site_pre = create :site
|
||||||
|
dup_name = "test-#{SecureRandom.hex}"
|
||||||
|
assert site_pre.deploys.create(type: 'DeployAlternativeDomain', hostname: "#{dup_name}.#{Site.domain}")
|
||||||
|
assert_not site.update(name: dup_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'al cambiar el nombre se crea un deploy alternativo' do
|
||||||
|
site_name = site.name
|
||||||
|
new_name = SecureRandom.hex
|
||||||
|
original_destination = site.deploy_local.destination
|
||||||
|
urls = [site.url]
|
||||||
|
|
||||||
|
assert site.deploy_local.deploy
|
||||||
|
assert_not site.deploy_local.destination_changed?
|
||||||
|
assert site.update(name: new_name)
|
||||||
|
|
||||||
|
urls << site.url
|
||||||
|
|
||||||
|
assert_equal urls.sort, site.urls.sort
|
||||||
|
assert File.symlink?(original_destination)
|
||||||
|
assert File.exist?(site.deploy_local.destination)
|
||||||
|
assert_equal 2, site.deploys.count
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'al cambiar el nombre se renombra el directorio' do
|
||||||
|
site_name = site.name
|
||||||
|
new_name = "test-#{SecureRandom.hex}"
|
||||||
|
original_destination = site.deploy_local.destination
|
||||||
|
|
||||||
|
assert site.deploy_local.deploy
|
||||||
|
assert_not site.deploy_local.destination_changed?
|
||||||
|
assert site.update(name: new_name)
|
||||||
|
assert site.deploy_local.hostname.start_with?(new_name)
|
||||||
|
assert File.symlink?(original_destination)
|
||||||
|
assert File.exist?(site.deploy_local.destination)
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'al cambiar el nombre se actualiza el www' do
|
||||||
|
site_name = site.name
|
||||||
|
new_name = "test-#{SecureRandom.hex}"
|
||||||
|
|
||||||
|
assert (deploy_www = site.deploys.create(type: 'DeployWww'))
|
||||||
|
assert site.deploy_local.deploy
|
||||||
|
assert_not site.deploy_local.destination_changed?
|
||||||
|
assert site.update(name: new_name)
|
||||||
|
assert deploy_www.reload.hostname.include?(new_name)
|
||||||
|
assert_equal 4, site.deploys.count
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'al cambiar el nombre varias veces se crean varios links' do
|
||||||
|
assert site.deploy_local.deploy
|
||||||
|
|
||||||
|
q = rand(3..10)
|
||||||
|
q.times do
|
||||||
|
assert site.update(name: "test-#{SecureRandom.hex}")
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_equal q, site.deploys.count
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'no se puede cambiar el nombre si ya existía un archivo en el mismo lugar' do
|
||||||
|
assert site.deploy_local.deploy
|
||||||
|
|
||||||
|
new_name = "test-#{SecureRandom.hex}"
|
||||||
|
FileUtils.mkdir File.join(Rails.root, '_deploy', "#{new_name}.#{Site.domain}")
|
||||||
|
|
||||||
|
assert_not site.update(name: new_name)
|
||||||
|
end
|
||||||
|
end
|
|
@ -26,18 +26,18 @@ class SiteTest < ActiveSupport::TestCase
|
||||||
assert_not site2.valid?
|
assert_not site2.valid?
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'el nombre del sitio puede contener subdominios' do
|
test 'el nombre del sitio no puede contener subdominios' do
|
||||||
@site = build :site, name: 'hola.chau'
|
@site = build :site, name: 'hola.chau'
|
||||||
site.validate
|
site.validate
|
||||||
|
|
||||||
assert_not site.errors.messages[:name].present?
|
assert site.errors.messages[:name].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'el nombre del sitio puede terminar con punto' do
|
test 'el nombre del sitio no puede terminar con punto' do
|
||||||
@site = build :site, name: 'hola.chau.'
|
@site = build :site, name: 'hola.chau.'
|
||||||
site.validate
|
site.validate
|
||||||
|
|
||||||
assert_not site.errors.messages[:name].present?
|
assert site.errors.messages[:name].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'el nombre del sitio no puede contener wildcard' do
|
test 'el nombre del sitio no puede contener wildcard' do
|
||||||
|
@ -93,9 +93,9 @@ class SiteTest < ActiveSupport::TestCase
|
||||||
test 'tienen un hostname que puede cambiar' do
|
test 'tienen un hostname que puede cambiar' do
|
||||||
assert_equal "#{site.name}.#{Site.domain}", site.hostname
|
assert_equal "#{site.name}.#{Site.domain}", site.hostname
|
||||||
|
|
||||||
site.name = name = SecureRandom.hex
|
site.update(name: (new_name = SecureRandom.hex))
|
||||||
|
|
||||||
assert_equal "#{name}.#{Site.domain}", site.hostname
|
assert_equal "#{new_name}.#{Site.domain}", site.hostname
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'se pueden traer los datos de una plantilla' do
|
test 'se pueden traer los datos de una plantilla' do
|
||||||
|
|
Loading…
Reference in a new issue