Skip to content

Instantly share code, notes, and snippets.

@matt-morris
Forked from ahoward/static-rails.rb
Last active September 10, 2015 20:33
Show Gist options
  • Save matt-morris/6d4c130c3a3d44a2739b to your computer and use it in GitHub Desktop.
Save matt-morris/6d4c130c3a3d44a2739b to your computer and use it in GitHub Desktop.
a script that boils a rails' app into static in a fucking hurry
#! /usr/bin/env ruby
# TODO - use a server other than passenger?
# this script uses wget and passenger to crawl a rails' application in order
# to create a static build from virtually any project.
#
# it has three simple requirements, and one optional one:
#
# 1) you have the wget program installed
#
# 2) you have
#
# gem 'passenger'
#
# in your Gemfile
#
# 3) you have configured your rails application such that urls end with
# trailing slashes. this is *** ÜBER IMPORTANT ***. putting the following in
# config/initializers/trailing_slashes.rb will do the trick.
#
# Rails.configuration.before_initialize do
#
# ActionController::Base.module_eval do
# before_filter :enforce_trailing_slash
#
# protected
#
# def enforce_trailing_slash
# if request.get?
# ext = request.fullpath.split('.', 2)[1]
#
# if ext.nil? and request.format.to_s == 'text/html'
# url = request.original_url
# url, qs = url.split('?')
#
# if !url.ends_with?('/')
# flash.keep
#
# url = url + '/'
#
# if qs
# url = url + '?' + qs
# end
#
# redirect_to(url)
#
# return
# end
# end
# end
# end
#
# end
#
# 4) although only required for development and test envs, if you want your
# site to build in development mode you'll need something like such that
# assets build correctly when rails is in development mode
#
# if ENV['RAILS_BUILD']
# config.serve_static_assets = true
# config.assets.compress = true
# config.assets.compile = true
# config.assets.digest = true
# config.assets.initialize_on_precompile = false
# config.assets.debug = false
# end
#
# in config/environments/development.rb, etc. the last line is ***VERY
# IMPORTANT***
#
#
#
#
# afterwards all you need to do is run
#
# ~> ./script/build
#
# and you'll have a preview-able static cache of all reachable pages in
#
# public/builds/$UUID
#
# check
#
# public/builds/$UUID/wget.oe
#
# if the build fails for any reasons. examine the detailed wget output for
# '404' or '500', etc.
#
# the build will be suitable for deployment to s3, bitballoon, etc. and will
# include all images, compiled assets, etc.
#
# make a build
@build = Build.new
@build.build!
puts @build.directory
BEGIN {
#
require 'fileutils'
require 'thread'
require 'socket'
require 'timeout'
require 'uri'
require 'open-uri'
require 'securerandom'
require 'rubygems'
require 'logger'
# awesome sauce static build support
#
class Build
#
attr_accessor :script_dir
attr_accessor :rails_root
attr_accessor :lib_dir
attr_accessor :directory
attr_accessor :uuid
attr_accessor :env
attr_accessor :passenger
attr_accessor :url
#
def initialize(*args, &block)
@options = args.last.is_a?(Hash) ? args.pop : {}
@logger = @options[:logger] || Logger.new(STDERR)
@script_dir = File.expand_path(File.dirname(__FILE__))
@rails_root = File.expand_path(File.dirname(@script_dir))
@lib_dir = File.join(@rails_root, 'lib')
@uuid = ENV['RAILS_BUILD'] || SecureRandom.uuid
@url = ENV['RAILS_BUILD_SERVER'] || ENV['RAILS_URL']
@env = ENV['RAILS_BUILD_ENV'] || ENV['RAILS_ENV'] || 'development'
@passenger = 'bundle exec passenger'
Dir.chdir(@rails_root)
if File.exists?('./Gemfile')
require 'bundler/setup'
Bundler.setup(:require => false)
end
$LOAD_PATH.unshift(@lib_dir)
@directory = File.join(@rails_root, 'public', 'system', 'builds', @uuid)
ENV['RAILS_BUILD'] ||= @uuid
ENV['RAILS_BUILD_ENV'] ||= @env
end
#
def build!
log(:debug, "building #{ @directory }...")
log(:debug, "locking...")
lock!
log(:debug, "locked.")
start_passenger! unless @url
wget_mirror!
normalize_files!
end
#
def lock!(max = 300)
started_at = Time.now.to_f
loop do
if DATA.flock(File::LOCK_EX | File::LOCK_NB)
break
else
warn "could not obtain lock for #{ max } seconds..."
if((Time.now.to_f - started_at) > max)
warn "ignoring failed lock!"
break
else
sleep(rand)
end
end
end
end
#
def start_passenger!
validate_passenger_version!
@url =
nil
passenger_port =
nil
ports =
(3001 .. 4001).to_a
ports.each do |port|
next unless port_open?(port)
start_passenger =
"#{ @passenger } start --daemonize --environment #{ @env } --port #{ port } --max-pool-size 16 --min-instances 16"
passenger_output =
`#{ start_passenger } 2>&1`.strip
t = Time.now.to_f
timeout = 10
i = 0
loop do
i += 1
begin
url = "http://0.0.0.0:#{ port }"
open(url){|socket| socket.read}
@url = url
passenger_port = port
break
rescue Object => e
if i > 2
log :error, "#{ e.message }(#{ e.class })\n"
log :error, "#{ passenger_output }\n\n"
end
if((Time.now.to_f - t) > timeout)
abort("could not start passenger inside of #{ timeout } ;-/")
else
sleep(rand(0.42))
end
end
end
break if @url
end
# barf if passenger could not be started
#
unless @url
abort("could not start passenger on any of ports #{ ports.first } .. #{ ports.last }")
end
log(:info, "started passenger on #{ @url }")
# set assassins to ensure the passenger daemon never outlives the build script
# no matter how it is killed (even -9)
#
stop_passenger =
"#{ @passenger } stop --port #{ passenger_port }"
at_exit{
`#{ stop_passenger } >/dev/null 2>&1`
log(:info, "stopped passenger on #{ @url }")
}
pppid = Process.pid
unless fork
at_exit{ exit! }
Process.setsid
if fork
exit
else
loop do
begin
Process.kill(0, pppid)
rescue Object => e
if e.is_a?(Errno::ESRCH)
`#{ stop_passenger } >/dev/null 2>&1`
end
exit
end
sleep(1 + rand)
end
end
end
end
#
def validate_passenger_version!
abort("could not find passenger") unless `#{ @passenger }`.to_s =~ /phusion\s+passenger/i
passenger_version = `#{ passenger } --version 2>/dev/null`.to_s.match(/version\s+([0-9.]+)/).to_a.last
abort("could not find passenger") unless passenger_version
major = passenger_version.to_s.split('.').first.to_i
abort("could not find passenger >= 4") unless(major && major >= 4)
end
#
def port_open?(port, options = {})
seconds = options[:timeout] || 1
ip = options[:ip] || '0.0.0.0'
Timeout::timeout(seconds) do
begin
TCPSocket.new(ip, port).close
false
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
true
rescue Object
false
end
end
rescue Timeout::Error
false
end
#
def wget_mirror!
FileUtils.rm_rf(@directory)
FileUtils.mkdir_p(@directory)
Dir.chdir(@directory) do
mirrored = false
a = Time.now
wget = "wget -m -r -E -nH -kK -l 42 --trust-server-names -P . #{ @url } > wget.oe 2>&1"
mirrored = system(wget)
b = Time.now
if mirrored
log(:info, "built site in #{ (b.to_f - a.to_f) }s")
else
log(:error, "failed to build site!")
end
end
end
#
def normalize_files!
glob = File.join(@directory, '**/**')
Dir.glob(glob) do |entry|
next unless test(?f, entry)
dirname = File.dirname(entry)
basename = File.basename(entry)
base, query = basename.split('?', 2)
if query
path = File.join(dirname, base)
FileUtils.cp(entry, path)
FileUtils.rm(entry)
end
end
end
#
def to_s
@directory.to_s
end
#
def log(level, *args, &block)
@logger.send(level, *args, &block)
end
end
}
__END__
__LOCK__
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment