mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-16 18:56:21 +00:00
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
|
||||
|
||||
# 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
|
||||
# tuvimos que poner 'sutty.nl..sutty.nl' para pasar el test.
|
||||
# Aunque en realidad con el hostname a partir del Origin nos
|
||||
# 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
|
||||
@site_id ||= if params[:site_id].end_with? Site.domain
|
||||
params[:site_id].sub(/\.#{Site.domain}\z/, '')
|
||||
else
|
||||
params[:site_id] + '.'
|
||||
end
|
||||
@site_id ||= Deploy.site_name_from_hostname(params[:site_id])
|
||||
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
|
||||
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
|
||||
|
||||
# Los navegadores antiguos no envían Origin
|
||||
|
|
|
@ -23,7 +23,7 @@ module Api
|
|||
contact_params.to_h.symbolize_keys,
|
||||
params[:redirect]
|
||||
|
||||
redirect_to params[:redirect] || origin.to_s
|
||||
redirect_to params[:redirect] || referer || site.url
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -44,11 +44,10 @@ module Api
|
|||
|
||||
# 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]
|
||||
def return_origin
|
||||
Rails.env.production? ? Site.find_by(name: site_id).url : origin
|
||||
site&.deploys&.find_by_hostname(origin_hostname)&.url
|
||||
end
|
||||
|
||||
# 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
|
||||
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-Credentials'] = true
|
||||
headers['Vary'] = 'Origin'
|
||||
|
|
|
@ -17,7 +17,7 @@ module Api
|
|||
site.touch if service.create_anonymous.persisted?
|
||||
|
||||
# Redirigir a la URL de agradecimiento
|
||||
redirect_to params[:redirect_to] || origin.to_s
|
||||
redirect_to params[:redirect_to] || referer || site.url
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -85,7 +85,9 @@ module Api
|
|||
# XXX: Este header se puede falsificar de todas formas pero al
|
||||
# menos es una trampa.
|
||||
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'
|
||||
render plain: Rails.env.production? ? nil : @reason, status: :precondition_required
|
||||
|
@ -116,11 +118,6 @@ module Api
|
|||
raise NotImplementedError
|
||||
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
|
||||
# quede asociado a ningún sitio.
|
||||
#
|
||||
|
|
|
@ -9,14 +9,14 @@ module Api
|
|||
|
||||
# Lista de nombres de dominios a emitir certificados
|
||||
def index
|
||||
render json: sites_names + alternative_names + api_names
|
||||
render json: Deploy.all.pluck(:hostname)
|
||||
end
|
||||
|
||||
# Sitios con hidden service de Tor
|
||||
#
|
||||
# @return [Array] lista de nombres de sitios sin onion aun
|
||||
def hidden_services
|
||||
render json: DeployHiddenService.where(values: nil).includes(:site).pluck(:name)
|
||||
render json: DeployHiddenService.temporary.includes(:site).pluck(:name)
|
||||
end
|
||||
|
||||
# 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] onion
|
||||
def add_onion
|
||||
site = Site.find_by(name: params[:name])
|
||||
|
||||
if site
|
||||
usuarie = GitAuthor.new email: 'tor@' + Site.domain, name: 'Tor'
|
||||
if (site = Site.find_by_name(params[:name]))
|
||||
usuarie = GitAuthor.new email: "tor@#{Site.domain}", name: 'Tor'
|
||||
service = SiteService.new site: site, usuarie: usuarie,
|
||||
params: params
|
||||
service.add_onion
|
||||
|
@ -36,28 +34,6 @@ module Api
|
|||
|
||||
head :ok
|
||||
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
|
||||
|
|
|
@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
before_action :prepare_exception_notifier
|
||||
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
|
||||
|
||||
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
||||
|
@ -16,7 +17,7 @@ class ApplicationController < ActionController::Base
|
|||
rescue_from ActionController::ParameterMissing, with: :page_not_found
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
end
|
||||
|
||||
# Encontrar un sitio por su nombre
|
||||
# Encontrar un sitio por su nombre.
|
||||
def find_site
|
||||
id = params[:site_id] || params[:id]
|
||||
|
||||
unless (site = current_usuarie&.sites&.find_by_name(id))
|
||||
raise SiteNotFound
|
||||
current_usuarie&.sites&.find_by_name(site_id).tap do |site|
|
||||
raise SiteNotFound unless site
|
||||
end
|
||||
|
||||
site
|
||||
end
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
def configure_permitted_parameters
|
||||
|
|
|
@ -7,7 +7,7 @@ class CollaborationsController < ApplicationController
|
|||
include Pundit
|
||||
|
||||
def collaborate
|
||||
@site = Site.find_by_name(params[:site_id])
|
||||
@site = find_site
|
||||
authorize Collaboration.new(@site)
|
||||
|
||||
@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
|
||||
def accept_collaboration
|
||||
@site = Site.find_by_name(params[:site_id])
|
||||
@site = find_site
|
||||
authorize Collaboration.new(@site)
|
||||
|
||||
@invitade = current_usuarie
|
||||
|
|
|
@ -136,8 +136,10 @@ class SitesController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def site
|
||||
@site ||= find_site
|
||||
# En los controladores dentro de este controlador vamos a usar :id
|
||||
# para obtener el nombre.
|
||||
def site_id
|
||||
@site_id ||= params[:site_id] || params[:id]
|
||||
end
|
||||
|
||||
def site_params
|
||||
|
|
|
@ -4,70 +4,63 @@
|
|||
class DeployJob < ApplicationJob
|
||||
class DeployException < StandardError; end
|
||||
|
||||
attr_reader :site, :deployed
|
||||
|
||||
# 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
|
||||
@site = Site.find(site)
|
||||
@site = Site.find(site_id)
|
||||
@deployed = {}
|
||||
|
||||
# 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?
|
||||
if 10.minutes.ago >= time
|
||||
@site.update status: 'waiting'
|
||||
site.update status: 'waiting'
|
||||
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
|
||||
|
||||
DeployJob.perform_in(60, site, notify, time)
|
||||
DeployJob.perform_in(60, site_id, notify, time)
|
||||
return
|
||||
end
|
||||
|
||||
@site.update status: 'building'
|
||||
site.update status: 'building'
|
||||
# Asegurarse que DeployLocal sea el primero!
|
||||
@deployed = { deploy_local: deploy_locally }
|
||||
deployed[:deploy_local] = site.deploy_local.deploy
|
||||
|
||||
# No es opcional
|
||||
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
|
||||
deploy_others if deployed[:deploy_local]
|
||||
|
||||
# Volver a la espera
|
||||
@site.update status: 'waiting'
|
||||
site.update status: 'waiting'
|
||||
|
||||
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
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
private
|
||||
|
||||
def deploy_local
|
||||
@deploy_local ||= @site.deploys.find_by(type: 'DeployLocal')
|
||||
end
|
||||
|
||||
def deploy_locally
|
||||
deploy_local.deploy
|
||||
end
|
||||
|
||||
# Correr todas las tareas que no sean el deploy local.
|
||||
def deploy_others
|
||||
@site.deploys.where.not(type: 'DeployLocal').find_each do |d|
|
||||
@deployed[d.type.underscore.to_sym] = d.deploy
|
||||
site.deploys.where.not(type: 'DeployLocal').find_each do |d|
|
||||
deployed[d.type.underscore.to_sym] = d.deploy
|
||||
end
|
||||
end
|
||||
|
||||
# Notificar a todes les usuaries no temporales.
|
||||
#
|
||||
# TODO: Poder configurar quiénes quieren recibir notificaciones.
|
||||
def notify_usuaries
|
||||
@site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie|
|
||||
DeployMailer.with(usuarie: usuarie, site: @site.id)
|
||||
.deployed(@deployed)
|
||||
site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie|
|
||||
DeployMailer.with(usuarie: usuarie, site: site.id)
|
||||
.deployed(deployed)
|
||||
.deliver_now
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ class DeployMailer < ApplicationMailer
|
|||
@usuarie = Usuarie.find(params[:usuarie])
|
||||
@site = @usuarie.sites.find(params[:site])
|
||||
@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
|
||||
# respuesta porque a veces les usuaries nos escriben
|
||||
|
|
|
@ -1,44 +1,138 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'open3'
|
||||
|
||||
# Este modelo implementa los distintos tipos de alojamiento que provee
|
||||
# Sutty.
|
||||
#
|
||||
# Los datos se guardan en la tabla `deploys`. Para guardar los
|
||||
# atributos, cada modelo tiene que definir su propio `store
|
||||
# :attributes`.
|
||||
# Cuando cambia el hostname de un Deploy, generamos un
|
||||
# DeployAlternativeDomain en su lugar. Esto permite que no se rompan
|
||||
# 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
|
||||
# Un sitio puede tener muchas formas de publicarse.
|
||||
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
|
||||
|
||||
# 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
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def limit
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# El espacio ocupado por este deploy.
|
||||
#
|
||||
# @return [Integer]
|
||||
def size
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Empezar a contar el tiempo
|
||||
#
|
||||
# @return [Time]
|
||||
def time_start
|
||||
@start = Time.now
|
||||
end
|
||||
|
||||
# Detener el contador
|
||||
#
|
||||
# @return [Time]
|
||||
def time_stop
|
||||
@stop = Time.now
|
||||
end
|
||||
|
||||
# Obtener la demora de la tarea
|
||||
#
|
||||
# @return [Float]
|
||||
def time_spent_in_seconds
|
||||
(@stop - @start).round(3)
|
||||
end
|
||||
|
||||
def home_dir
|
||||
site.path
|
||||
end
|
||||
|
||||
# El directorio donde se almacenan las gemas.
|
||||
#
|
||||
# TODO: En un momento podíamos tenerlas todas compartidas y ahorrar
|
||||
# espacio, pero bundler empezó a mezclar cosas.
|
||||
#
|
||||
# @return [String]
|
||||
def gems_dir
|
||||
@gems_dir ||= Rails.root.join('_storage', 'gems', site.name)
|
||||
end
|
||||
|
@ -77,9 +171,67 @@ class Deploy < ApplicationRecord
|
|||
|
||||
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]
|
||||
# @return [String]
|
||||
def readable_cmd(cmd)
|
||||
cmd.split(' -', 2).first.tr(' ', '_')
|
||||
end
|
||||
|
||||
# Cada deploy puede decidir su propia validación
|
||||
#
|
||||
# @return [Boolean]
|
||||
def implements_hostname_validation?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Soportar dominios alternativos
|
||||
class DeployAlternativeDomain < Deploy
|
||||
store :values, accessors: %i[hostname], coder: JSON
|
||||
# Soportar dominios alternativos.
|
||||
class DeployAlternativeDomain < DeployWww
|
||||
validates :hostname, domainname: true
|
||||
|
||||
# Generar un link simbólico del sitio principal al alternativo
|
||||
def deploy
|
||||
File.symlink?(destination) ||
|
||||
File.symlink(site.hostname, destination).zero?
|
||||
# No hay un hostname por defecto
|
||||
#
|
||||
# @return [Nil]
|
||||
def default_hostname; end
|
||||
|
||||
private
|
||||
|
||||
def implements_hostname_validation?
|
||||
true
|
||||
end
|
||||
|
||||
# No hay límite para los dominios alternativos
|
||||
def limit; end
|
||||
|
||||
def size
|
||||
File.size destination
|
||||
end
|
||||
|
||||
def destination
|
||||
File.join(Rails.root, '_deploy', hostname.gsub(/\.\z/, ''))
|
||||
end
|
||||
# No hay un hostname por defecto. Debe ser informado por les
|
||||
# usuaries.
|
||||
def default_hostname!; end
|
||||
end
|
||||
|
|
|
@ -1,18 +1,61 @@
|
|||
# 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
|
||||
def deploy
|
||||
return true if fqdn.blank?
|
||||
validates :hostname, format: { with: /\A[a-z2-7]{56}.onion\z/ }
|
||||
|
||||
super
|
||||
end
|
||||
# Sufijo para todos los dominios temporales.
|
||||
TEMPORARY_SUFFIX = 'temporary'
|
||||
|
||||
def fqdn
|
||||
values[:onion]
|
||||
end
|
||||
# Traer todos los servicios ocultos temporales.
|
||||
scope :temporary, -> { where("hostname not like '#{TEMPORARY_SUFFIX}%'") }
|
||||
|
||||
# Los servicios ocultos son su propio transporte cifrado y
|
||||
# autenticado.
|
||||
#
|
||||
# @return [String]
|
||||
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
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Alojamiento local, solo genera el sitio, con lo que no necesita hacer
|
||||
# nada más
|
||||
# Alojamiento local, genera el sitio como si corriéramos `jekyll build`.
|
||||
class DeployLocal < Deploy
|
||||
store :values, accessors: %i[], coder: JSON
|
||||
|
||||
before_destroy :remove_destination!
|
||||
# Asegurarse que el hostname es el permitido.
|
||||
before_validation :reset_hostname!, :default_hostname!
|
||||
# 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
|
||||
# limpio para no pasarle secretos
|
||||
|
@ -20,42 +20,52 @@ class DeployLocal < Deploy
|
|||
jekyll_build
|
||||
end
|
||||
|
||||
# Sólo permitimos un deploy local
|
||||
def limit
|
||||
1
|
||||
end
|
||||
|
||||
# Obtener el tamaño de todos los archivos y directorios (los
|
||||
# directorios son archivos :)
|
||||
#
|
||||
# @return [Integer]
|
||||
def size
|
||||
paths = [destination, File.join(destination, '**', '**')]
|
||||
|
||||
Dir.glob(paths).map do |file|
|
||||
if File.symlink? file
|
||||
0
|
||||
else
|
||||
File.size(file)
|
||||
end
|
||||
File.symlink?(file) ? 0 : File.size(file)
|
||||
end.inject(:+)
|
||||
end
|
||||
|
||||
def destination
|
||||
File.join(Rails.root, '_deploy', site.hostname)
|
||||
# El hostname es el nombre del sitio más el dominio principal.
|
||||
#
|
||||
# @return [String]
|
||||
def default_hostname
|
||||
"#{site.name}.#{Site.domain}"
|
||||
end
|
||||
|
||||
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
|
||||
FileUtils.mkdir_p destination
|
||||
end
|
||||
|
||||
# Un entorno que solo tiene lo que necesitamos
|
||||
#
|
||||
# @return [Hash]
|
||||
def env
|
||||
# XXX: This doesn't support Windows paths :B
|
||||
paths = [File.dirname(`which bundle`), '/usr/bin', '/bin']
|
||||
|
||||
{
|
||||
'HOME' => home_dir,
|
||||
'HOME' => site.path,
|
||||
'PATH' => paths.join(':'),
|
||||
'SPREE_API_KEY' => site.tienda_api_key,
|
||||
'SPREE_URL' => site.tienda_url,
|
||||
|
@ -66,10 +76,15 @@ class DeployLocal < Deploy
|
|||
}
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
def yarn_lock
|
||||
File.join(site.path, 'yarn.lock')
|
||||
end
|
||||
|
||||
# Determina si este proyecto se gestiona con Yarn, buscando si el
|
||||
# archivo yarn.lock existe.
|
||||
#
|
||||
# @return [Boolean]
|
||||
def yarn_lock?
|
||||
File.exist? yarn_lock
|
||||
end
|
||||
|
@ -79,12 +94,17 @@ class DeployLocal < Deploy
|
|||
end
|
||||
|
||||
# Corre yarn dentro del repositorio
|
||||
#
|
||||
# @return [Boolean,Nil]
|
||||
def yarn
|
||||
return true unless yarn_lock?
|
||||
|
||||
run 'yarn'
|
||||
end
|
||||
|
||||
# Instala las dependencias.
|
||||
#
|
||||
# @return [Boolean]
|
||||
def bundle
|
||||
if Rails.env.production?
|
||||
run %(bundle install --no-cache --path="#{gems_dir}")
|
||||
|
@ -93,6 +113,9 @@ class DeployLocal < Deploy
|
|||
end
|
||||
end
|
||||
|
||||
# Genera el sitio.
|
||||
#
|
||||
# @return [Boolean]
|
||||
def jekyll_build
|
||||
run %(bundle exec jekyll build --trace --profile --destination "#{escaped_destination}")
|
||||
end
|
||||
|
|
|
@ -11,12 +11,31 @@ class DeployPrivate < DeployLocal
|
|||
jekyll_build
|
||||
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
|
||||
File.join(Rails.root, '_private', site.name)
|
||||
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
|
||||
#
|
||||
# @return [Hash]
|
||||
def env
|
||||
@env ||= super.merge({
|
||||
'JEKYLL_ENV' => 'development',
|
||||
|
|
|
@ -2,34 +2,48 @@
|
|||
|
||||
# Vincula la versión del sitio con www a la versión sin
|
||||
class DeployWww < Deploy
|
||||
store :values, accessors: %i[], coder: JSON
|
||||
|
||||
before_destroy :remove_destination!
|
||||
|
||||
# La forma de hacer este deploy es generar un link simbólico entre el
|
||||
# directorio canónico y el actual.
|
||||
#
|
||||
# @return [Boolean]
|
||||
def deploy
|
||||
File.symlink?(destination) ||
|
||||
File.symlink(site.hostname, destination).zero?
|
||||
end
|
||||
|
||||
def limit
|
||||
1
|
||||
# Eliminar los links rotos
|
||||
remove_destination! if broken?
|
||||
|
||||
# No hacer nada si ya existe.
|
||||
return true if exist?
|
||||
|
||||
# Generar un link simbólico con la ruta relativa al destino
|
||||
File.symlink(relative_path, destination).zero?
|
||||
end
|
||||
|
||||
# Siempre devuelve el espacio ocupado por el link simbólico, no el
|
||||
# destino.
|
||||
#
|
||||
# @return [Integer]
|
||||
def size
|
||||
File.size destination
|
||||
relative_path.size
|
||||
end
|
||||
|
||||
def destination
|
||||
File.join(Rails.root, '_deploy', fqdn)
|
||||
end
|
||||
|
||||
def fqdn
|
||||
"www.#{site.hostname}"
|
||||
# El hostname por defecto incluye WWW
|
||||
#
|
||||
# @return [String]
|
||||
def default_hostname
|
||||
"www.#{site.deploy_local.hostname}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Elimina el link simbólico si se elimina este deploy.
|
||||
def remove_destination!
|
||||
FileUtils.rm_f destination
|
||||
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
|
||||
|
|
|
@ -2,22 +2,24 @@
|
|||
|
||||
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
|
||||
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
|
||||
# y generar un zip accesible públicamente.
|
||||
# y generar un ZIP accesible públicamente.
|
||||
#
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
# @return [Boolean]
|
||||
def deploy
|
||||
FileUtils.rm_f path
|
||||
remove_destination!
|
||||
|
||||
time_start
|
||||
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|
|
||||
File.directory?(f) ? z.mkdir(f) : z.add(f, f)
|
||||
end
|
||||
|
@ -31,25 +33,47 @@ class DeployZip < Deploy
|
|||
|
||||
File.exist? path
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
def limit
|
||||
1
|
||||
# La URL de descarga del archivo.
|
||||
#
|
||||
# @return [String]
|
||||
def url
|
||||
"#{site.deploy_local.url}/#{hostname}"
|
||||
end
|
||||
|
||||
# Devuelve el tamaño del ZIP en bytes
|
||||
#
|
||||
# @return [Integer]
|
||||
def size
|
||||
File.size path
|
||||
end
|
||||
|
||||
# El archivo ZIP se guarda dentro del sitio local para poder
|
||||
# descargarlo luego.
|
||||
#
|
||||
# @return [String]
|
||||
def destination
|
||||
File.join(Rails.root, '_deploy', site.hostname)
|
||||
site.deploy_local.destination
|
||||
end
|
||||
|
||||
def file
|
||||
"#{site.hostname}.zip"
|
||||
# El "hostname" es la ubicación del archivo.
|
||||
#
|
||||
# @return [String]
|
||||
def default_hostname
|
||||
"#{site.deploy_local.hostname}.zip"
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -7,6 +7,7 @@ class Site < ApplicationRecord
|
|||
include Site::Forms
|
||||
include Site::FindAndReplace
|
||||
include Site::Api
|
||||
include Site::Deployment
|
||||
include Tienda
|
||||
|
||||
# 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!
|
||||
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_uniqueness_of :name
|
||||
validates_inclusion_of :status, in: %w[waiting enqueued building]
|
||||
validates_presence_of :title
|
||||
validates :description, length: { in: 50..160 }
|
||||
validate :deploy_local_presence
|
||||
validate :compatible_layouts, on: :update
|
||||
|
||||
attr_reader :incompatible_layouts
|
||||
|
@ -38,8 +31,6 @@ class Site < ApplicationRecord
|
|||
belongs_to :licencia
|
||||
|
||||
has_many :log_entries, dependent: :destroy
|
||||
has_many :deploys, dependent: :destroy
|
||||
has_many :build_stats, through: :deploys
|
||||
has_many :roles, dependent: :destroy
|
||||
has_many :usuaries, -> { where('roles.rol = ?', 'usuarie') },
|
||||
through: :roles
|
||||
|
@ -58,13 +49,11 @@ class Site < ApplicationRecord
|
|||
after_initialize :load_jekyll
|
||||
after_create :load_jekyll, :static_file_migration!
|
||||
# Cambiar el nombre del directorio
|
||||
before_update :update_name!
|
||||
before_update :update_name!, if: :name_changed?
|
||||
before_save :add_private_key_if_missing!
|
||||
# Guardar la configuración si hubo cambios
|
||||
after_save :sync_attributes_with_config!
|
||||
|
||||
accepts_nested_attributes_for :deploys, allow_destroy: true
|
||||
|
||||
# El sitio en Jekyll
|
||||
attr_reader :jekyll
|
||||
|
||||
|
@ -85,49 +74,6 @@ class Site < ApplicationRecord
|
|||
@repository ||= Site::Repository.new path
|
||||
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)
|
||||
!invitades.find_by(id: usuarie.id).nil?
|
||||
end
|
||||
|
@ -453,8 +399,6 @@ class Site < ApplicationRecord
|
|||
end
|
||||
|
||||
def update_name!
|
||||
return unless name_changed?
|
||||
|
||||
FileUtils.mv path_was, path
|
||||
reload_jekyll!
|
||||
end
|
||||
|
@ -477,19 +421,6 @@ class Site < ApplicationRecord
|
|||
Site::StaticFileMigration.new(site: self).migrate!
|
||||
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
|
||||
# inexistentes.
|
||||
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
|
||||
site.save &&
|
||||
site.config.write &&
|
||||
commit_config(action: :create)
|
||||
commit_config(action: :create) &&
|
||||
add_licencias
|
||||
end
|
||||
|
||||
add_licencias
|
||||
|
||||
site
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
%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]
|
||||
|
||||
%table
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
= '# ' + t('.hi')
|
||||
\
|
||||
= t('.explanation', fqdn: @deploy_local.site.hostname)
|
||||
= t('.explanation', url: @site.deploy_local.url)
|
||||
\
|
||||
= Terminal::Table.new do |table|
|
||||
- table << [t('.th.type'), t('.th.status')]
|
||||
|
|
|
@ -14,10 +14,10 @@
|
|||
'0', '1'
|
||||
= deploy.label :_destroy, class: 'custom-control-label' do
|
||||
%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]
|
||||
|
||||
- if deploy.object.fqdn
|
||||
- unless deploy.object.temporary?
|
||||
= sanitize_markdown t('.help_2', url: deploy.object.url),
|
||||
tags: %w[p strong em a]
|
||||
%hr/
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
.row
|
||||
.col
|
||||
%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]
|
||||
|
||||
= deploy.hidden_field :type
|
||||
-# No duplicarlos una vez que existen.
|
||||
- unless deploy.object.persisted?
|
||||
= deploy.hidden_field :type
|
||||
|
|
|
@ -15,6 +15,6 @@
|
|||
'0', '1'
|
||||
= deploy.label :_destroy, class: 'custom-control-label' do
|
||||
%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]
|
||||
%hr/
|
||||
|
|
|
@ -15,10 +15,5 @@
|
|||
'0', '1'
|
||||
= deploy.label :_destroy, class: 'custom-control-label' do
|
||||
%h3= t('.title')
|
||||
-# TODO: secar la generación de URLs
|
||||
- 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]
|
||||
= sanitize_markdown t('.help', url: deploy.object.url), tags: %w[p strong em a]
|
||||
%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
|
||||
# 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
|
||||
# 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
|
||||
# el servidor no tiene el Preflight cacheado.
|
||||
#
|
||||
# TODO: Limitar el acceso desde Nginx también.
|
||||
#
|
||||
# TODO: Poder consultar por sitios por todas sus URLs posibles.
|
||||
origins do |source, _|
|
||||
# Cacheamos la respuesta para no tener que volver a procesarla
|
||||
# cada vez.
|
||||
Rails.cache.fetch(source, expires_in: 1.hour) do
|
||||
uri = URI(source)
|
||||
|
||||
if (name = uri&.host&.split('.', 2)&.first).present?
|
||||
Site.where(name: [name, uri.host + '.']).pluck(:name).first.present?
|
||||
else
|
||||
false
|
||||
end
|
||||
rescue URI::Error
|
||||
hostname = URI(source)&.host
|
||||
hostname.present? && Deploy.find_by_hostname(hostname).present?
|
||||
rescue StandardError
|
||||
false
|
||||
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!"
|
||||
explanation: |
|
||||
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.
|
||||
th:
|
||||
|
@ -143,8 +143,15 @@ en:
|
|||
tienda_api_key: Store access key
|
||||
errors:
|
||||
models:
|
||||
deploy:
|
||||
attributes:
|
||||
hostname:
|
||||
destination_exist: 'There already is a file in the destination'
|
||||
site:
|
||||
attributes:
|
||||
name:
|
||||
no_subdomains: 'Name cannot contain dots'
|
||||
duplicated_hostname: 'There already is a site with this address'
|
||||
deploys:
|
||||
deploy_local_presence: 'We need to be build the site!'
|
||||
design_id:
|
||||
|
@ -195,7 +202,7 @@ en:
|
|||
deploy_local:
|
||||
title: 'Host at Sutty'
|
||||
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
|
||||
domains, you can [help us](https://sutty.nl/en/index.html#contact)!
|
||||
|
@ -211,7 +218,7 @@ en:
|
|||
title: 'Add www to the address'
|
||||
help: |
|
||||
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
|
||||
computers that are available on the World Wide Web. Since
|
||||
|
@ -222,7 +229,7 @@ en:
|
|||
help: |
|
||||
ZIP files contain and compress all your site's files. With
|
||||
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
|
||||
share a copy of your site.
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ es:
|
|||
hi: "¡Hola!"
|
||||
explanation: |
|
||||
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.
|
||||
th:
|
||||
|
@ -143,8 +143,15 @@ es:
|
|||
tienda_api_key: Clave de acceso
|
||||
errors:
|
||||
models:
|
||||
deploy:
|
||||
attributes:
|
||||
hostname:
|
||||
destination_exist: 'Ya hay un archivo en esta ubicación'
|
||||
site:
|
||||
attributes:
|
||||
name:
|
||||
no_subdomains: 'El nombre no puede contener puntos'
|
||||
duplicated_hostname: 'Ya existe un sitio con ese nombre'
|
||||
deploys:
|
||||
deploy_local_presence: '¡Necesitamos poder generar el sitio!'
|
||||
design_id:
|
||||
|
@ -197,7 +204,7 @@ es:
|
|||
deploy_local:
|
||||
title: 'Alojar en Sutty'
|
||||
help: |
|
||||
El sitio estará disponible en <https://%{fqdn}/>.
|
||||
El sitio estará disponible en <%{url}/>.
|
||||
|
||||
Estamos desarrollando la posibilidad de agregar tus propios
|
||||
dominios, ¡ayudanos!
|
||||
|
@ -213,7 +220,7 @@ es:
|
|||
title: 'Agregar www a la dirección'
|
||||
help: |
|
||||
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
|
||||
referirse a las computadoras que están disponibles en la _World
|
||||
|
@ -226,7 +233,7 @@ es:
|
|||
help: |
|
||||
Los archivos ZIP contienen y comprimen todos los archivos de tu
|
||||
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
|
||||
alojamiento solidario, donde muchas personas comparten una copia
|
||||
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
|
||||
|
||||
test 'el sitio tiene que existir' do
|
||||
hostname = @site.hostname
|
||||
@site.destroy
|
||||
|
||||
get v1_site_contact_cookie_url(@site.hostname, **@host)
|
||||
get v1_site_contact_cookie_url(hostname, **@host)
|
||||
|
||||
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: {
|
||||
name: SecureRandom.hex,
|
||||
pronouns: SecureRandom.hex,
|
||||
|
@ -106,7 +107,7 @@ module Api
|
|||
test 'se puede enviar mensajes a dominios propios' do
|
||||
ActionMailer::Base.deliveries.clear
|
||||
|
||||
@site.update name: 'example.org.'
|
||||
@site.update name: 'example'
|
||||
|
||||
redirect = "#{@site.url}?thanks"
|
||||
|
||||
|
@ -130,6 +131,34 @@ module Api
|
|||
assert_equal redirect, response.headers['Location']
|
||||
assert_equal 2, ActionMailer::Base.deliveries.size
|
||||
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
|
||||
|
|
|
@ -23,7 +23,7 @@ module Api
|
|||
|
||||
test 'se puede obtener un listado de todos' do
|
||||
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
|
||||
|
|
|
@ -119,12 +119,7 @@ class SitesControllerTest < ActionDispatch::IntegrationTest
|
|||
title: name,
|
||||
description: name * 2,
|
||||
design_id: design.id,
|
||||
licencia_id: Licencia.all.second.id,
|
||||
deploys_attributes: {
|
||||
'0' => {
|
||||
type: 'DeployLocal'
|
||||
}
|
||||
}
|
||||
licencia_id: Licencia.all.second.id
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,11 +9,11 @@ FactoryBot.define do
|
|||
licencia
|
||||
|
||||
after :build do |site|
|
||||
site.deploys << build(:deploy_local, site: site)
|
||||
end
|
||||
|
||||
after :create do |site|
|
||||
site.deploys << create(:deploy_local, site: site)
|
||||
# XXX: Generamos un DeployLocal normalmente y no a través de una
|
||||
# Factory porque necesitamos que el sitio se genere solo.
|
||||
#
|
||||
# @see {https://github.com/thoughtbot/factory_bot/wiki/How-factory_bot-interacts-with-ActiveRecord}
|
||||
site.deploys.build(type: 'DeployLocal')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,11 +2,7 @@
|
|||
|
||||
class DeployJobTest < ActiveSupport::TestCase
|
||||
test 'se puede compilar' do
|
||||
rol = create :rol
|
||||
site = rol.site
|
||||
site.deploys << create(:deploy_zip, site: site)
|
||||
|
||||
site.save
|
||||
site = create :site
|
||||
|
||||
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
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
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
|
||||
site = create :site
|
||||
local = create :deploy_local, site: site
|
||||
deploy = create :deploy_zip, site: site
|
||||
deploy_local = @site.deploy_local
|
||||
|
||||
# Primero tenemos que generar el sitio
|
||||
local.deploy
|
||||
assert 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
|
||||
assert File.file?(deploy.path)
|
||||
assert_equal 'application/zip',
|
||||
`file --mime-type "#{escaped_path}"`.split(' ').last
|
||||
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
|
||||
test 'al eliminarlos se elimina el directorio' do
|
||||
deploy_local = @site.deploy_local
|
||||
assert deploy_local.destroy
|
||||
assert_not File.directory?(deploy_local.destination)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,17 +3,23 @@
|
|||
require 'test_helper'
|
||||
|
||||
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
|
||||
site = create :site
|
||||
local = create :deploy_local, site: site
|
||||
deploy = create :deploy_www, site: site
|
||||
assert @site.deploy_local.deploy
|
||||
|
||||
# Primero tenemos que generar el sitio
|
||||
local.deploy
|
||||
|
||||
assert deploy.deploy
|
||||
assert File.symlink?(deploy.destination)
|
||||
|
||||
local.destroy
|
||||
assert @deploy_www.deploy
|
||||
assert File.symlink?(@deploy_www.destination)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,18 +3,29 @@
|
|||
require 'test_helper'
|
||||
|
||||
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
|
||||
deploy_local = create :deploy_local
|
||||
# Primero tenemos que generar el sitio
|
||||
assert @site.deploy_local.deploy
|
||||
|
||||
assert 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
|
||||
|
||||
assert deploy_local.build_stats.map(&:bytes).compact.inject(:+).positive?
|
||||
assert deploy_local.build_stats.map(&:seconds).compact.inject(:+).positive?
|
||||
|
||||
assert deploy_local.destroy
|
||||
assert_not File.directory?(deploy_local.destination)
|
||||
assert @deploy_zip.deploy
|
||||
assert File.file?(@deploy_zip.path)
|
||||
assert_equal 'application/zip',
|
||||
`file --mime-type "#{@deploy_zip.path}"`.split.last
|
||||
assert_equal 1, @deploy_zip.build_stats.count
|
||||
assert @deploy_zip.build_stats.map(&:bytes).inject(:+).positive?
|
||||
assert @deploy_zip.build_stats.map(&:seconds).inject(:+).positive?
|
||||
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?
|
||||
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.validate
|
||||
|
||||
assert_not site.errors.messages[:name].present?
|
||||
assert site.errors.messages[:name].present?
|
||||
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.validate
|
||||
|
||||
assert_not site.errors.messages[:name].present?
|
||||
assert site.errors.messages[:name].present?
|
||||
end
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
test 'se pueden traer los datos de una plantilla' do
|
||||
|
|
Loading…
Reference in a new issue