adva-static 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,92 @@
1
+ module Adva
2
+ class Static
3
+ class Import
4
+ class Request
5
+ attr_reader :source, :record, :attributes
6
+
7
+ def initialize(source, record, attributes)
8
+ @source = source
9
+ @record = record
10
+ @attributes = attributes
11
+ end
12
+
13
+ def params
14
+ @params ||= begin
15
+ key = model_name.underscore.to_sym
16
+ if destroy?
17
+ params = { '_method' => 'delete', key => { :id => record.id } }
18
+ else
19
+ params = { model_name.underscore.to_sym => attributes }
20
+ params.merge!('_method' => 'put') if update?
21
+ end
22
+ stringify(params)
23
+ end
24
+ end
25
+
26
+ def path
27
+ controller.polymorphic_path(controller.resources)
28
+ end
29
+
30
+ def public_path
31
+ controller.public_url_for(controller.resources, :routing_type => :path)
32
+ end
33
+
34
+ def create?
35
+ !update? && !destroy?
36
+ end
37
+
38
+ def update?
39
+ record.persisted? && source.exist?
40
+ end
41
+
42
+ def destroy?
43
+ record.persisted? && !source.exist?
44
+ end
45
+
46
+ def controller
47
+ @controller ||= controller_name.constantize.new.tap do |controller|
48
+ controller.request = ActionDispatch::TestRequest.new
49
+ controller.params = params_for(controller)
50
+ end
51
+ end
52
+
53
+ protected
54
+
55
+ def section_ids
56
+ @section_ids ||= Section.types.map { |type| :"#{type.underscore}_id" }
57
+ end
58
+
59
+ def model_name
60
+ record.class.name
61
+ end
62
+
63
+ def controller_name
64
+ "Admin::#{model_name.pluralize}Controller"
65
+ end
66
+
67
+ def params_for(controller)
68
+ names = controller.send(:symbols_for_association_chain).dup
69
+ names.map! { |name| :"#{name}_id" }
70
+ names << :id unless record.new_record?
71
+
72
+ names.inject(:action => record.new_record? ? :index : :show) do |params, name|
73
+ # umm. admin blog routes use :blog_id, but Post has a section_id
74
+ value = attributes[section_ids.include?(name) ? :section_id : name].to_s
75
+ params.merge(name => value)
76
+ end
77
+ end
78
+
79
+ def stringify(object)
80
+ case object
81
+ when Hash
82
+ object.each { |key, value| object[key] = stringify(value) }
83
+ when Array
84
+ object.map! { |element| stringify(element) }
85
+ else
86
+ object.to_s
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,82 @@
1
+ module Adva
2
+ class Static
3
+ class Import
4
+ class Source < Pathname
5
+ TYPES = ['html', 'jekyll', 'yml']
6
+ EXTENSIONS = TYPES.map { |type| ".#{type}" }
7
+
8
+ attr_reader :root
9
+
10
+ delegate :exist?, :to => :full_path
11
+
12
+ def initialize(path, root = nil)
13
+ root ||= path.root if path.respond_to?(:root)
14
+ @root = Pathname.new(root.to_s)
15
+
16
+ path = path.to_s.gsub(root, '') if root
17
+ path = path.to_s[1..-1] if path.to_s[0, 1] == '/'
18
+ super(path)
19
+ end
20
+
21
+ def find_or_self
22
+ find or self
23
+ end
24
+
25
+ def find
26
+ file = Dir["#{root.join(path)}.{#{TYPES.join(',')}}"].first
27
+ Source.new(file, root) if file
28
+ end
29
+
30
+ def all
31
+ @all ||= Dir[root.join(path).join("**/*.{#{TYPES.join(',')}}")].map { |path| Source.new(path, root) }
32
+ end
33
+
34
+ def files
35
+ files = path == 'index' ? directory.all : all
36
+ files.reject { |path| path.basename == 'site' }.sort
37
+ end
38
+
39
+ def root?
40
+ @_root ||= path == 'index' || full_path.to_s == root.to_s
41
+ end
42
+
43
+ def directory
44
+ @directory ||= self.class.new(dirname, root)
45
+ end
46
+
47
+ def basename
48
+ @basename ||= super.to_s.sub(/\.\w+$/, '')
49
+ end
50
+
51
+ def dirname
52
+ @dirname ||= super.to_s.sub(/^.$/, '')
53
+ end
54
+
55
+ def path
56
+ @_path ||= [dirname, basename].select(&:present?).join('/')
57
+ end
58
+
59
+ def full_path
60
+ @full_path ||= root.join(self)
61
+ end
62
+
63
+ def self_and_parents
64
+ parents << self
65
+ end
66
+
67
+ def parents
68
+ @parents ||= begin
69
+ parts = self.to_s.split('/')[0..-2]
70
+ parts.inject([]) do |parents, part|
71
+ parents << Source.new(parts[0..parents.size].join('/'), root)
72
+ end
73
+ end
74
+ end
75
+
76
+ def <=>(other)
77
+ path == 'index' ? -1 : other.path == 'index' ? 1 : path <=> other.path
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,42 @@
1
+ module Adva
2
+ class Static
3
+ class Import
4
+ autoload :Format, 'adva/static/import/format'
5
+ autoload :Model, 'adva/static/import/model'
6
+ autoload :Request, 'adva/static/import/request'
7
+ autoload :Source, 'adva/static/import/source'
8
+
9
+ attr_reader :root
10
+
11
+ def initialize(options = {})
12
+ @root = Pathname.new(File.expand_path(options[:source] || 'import'))
13
+ end
14
+
15
+ def run
16
+ Adva.out.puts "importing from #{root}"
17
+ Account.all.each(&:destroy)
18
+ Model::Site.new(root).updated_record.save!
19
+ end
20
+
21
+ def import(path)
22
+ model = recognize(path).first
23
+ model.updated_record.save! if model
24
+ end
25
+
26
+ def request_for(path)
27
+ model = recognize(path).first
28
+ Request.new(model.source, model.record, model.attributes)
29
+ end
30
+
31
+ protected
32
+
33
+ def source(path)
34
+ Source.new(path, root)
35
+ end
36
+
37
+ def recognize(path)
38
+ Model.recognize([source(path)])
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,59 @@
1
+ require 'fileutils'
2
+
3
+ module Adva
4
+ class Static
5
+ module Rack
6
+ class Export
7
+ include Request
8
+
9
+ attr_reader :app, :target, :store
10
+
11
+ def initialize(app, options = {})
12
+ @app = app
13
+ @target = Pathname.new(options[:target] || File.expand_path('./export'))
14
+ @store = Adva::Static::Export::Store.new(target)
15
+ end
16
+
17
+ def call(env)
18
+ path = env['PATH_INFO'].dup # gets modified by routing_filter
19
+ app.call(env).tap do |status, headers, response|
20
+ export(path, response) if export?(env, status)
21
+ if headers.key?(PURGE_HEADER)
22
+ paths = normalize_paths(headers[PURGE_HEADER])
23
+ paths.each do |path|
24
+ purge(path)
25
+ request(path)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ protected
32
+
33
+ def export?(env, status)
34
+ env[STORE_HEADER].present? and status == 200
35
+ end
36
+
37
+ def export(path, response)
38
+ page = Adva::Static::Export::Page.new(path, response)
39
+ Adva.out.puts " storing #{page.url.filename}"
40
+ store.write(page.url, page.body)
41
+ end
42
+
43
+ def purge(path)
44
+ Adva.out.puts " purging #{path}"
45
+ store.purge(Adva::Static::Export::Path.new(path))
46
+ end
47
+
48
+ def request(path)
49
+ super('GET', Adva::Static::Export::Path.new(path), STORE_HEADER => true)
50
+ end
51
+
52
+ def normalize_paths(paths)
53
+ paths = paths.split("\n") if paths.is_a?(String)
54
+ Array(paths)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,39 @@
1
+ require 'rack/utils'
2
+
3
+ module Adva
4
+ class Static
5
+ module Rack
6
+ module Request
7
+ protected
8
+ def request(method, path, params = {})
9
+ Adva.out.puts " #{params['_method'] ? params['_method'].upcase : method} #{path} "
10
+ call(env_for(method, path, params)).tap do |status, headers, response|
11
+ Adva.out.puts " => #{status} " + (status == 302 ? "(Location: #{headers['Location']})" : '')
12
+ Adva.out.puts response if status == 500
13
+ end
14
+ end
15
+
16
+ def env_for(method, path, params)
17
+ ::Rack::MockRequest.env_for("http://#{site.host}#{path}", :method => method,
18
+ :input => ::Rack::Utils.build_nested_query(params),
19
+ 'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
20
+ 'HTTP_AUTHORIZATION' => 'Basic ' + ["#{username}:#{password}"].pack('m*'),
21
+ STORE_HEADER => params[STORE_HEADER]
22
+ )
23
+ end
24
+
25
+ def username
26
+ 'admin@admin.org' # TODO read from conf/auth.yml or something
27
+ end
28
+
29
+ def password
30
+ 'admin!'
31
+ end
32
+
33
+ def site
34
+ @site ||= Site.first || raise('could not find any site') # FIXME
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ require 'rack/utils'
2
+
3
+ module Adva
4
+ class Static
5
+ module Rack
6
+ class Static < ::Rack::File
7
+ attr_reader :app, :root
8
+
9
+ def initialize(app, root)
10
+ @app = app
11
+ @root = root
12
+ Adva.out.puts "serving from #{root}"
13
+ end
14
+
15
+ def call(env)
16
+ if get?(env) && path = static(env)
17
+ super(env.merge('PATH_INFO' => path))
18
+ else
19
+ app.call(env)
20
+ end
21
+ end
22
+
23
+ protected
24
+
25
+ def static(env)
26
+ path = env['PATH_INFO'].chomp('/')
27
+ [path, "#{path}.html", "#{path}/index.html"].detect { |path| file?(path) }
28
+ end
29
+
30
+ def file?(path)
31
+ File.file?(File.join(root, ::Rack::Utils.unescape(path)))
32
+ end
33
+
34
+ def get?(env)
35
+ env['REQUEST_METHOD'] == 'GET'
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,88 @@
1
+ require 'watchr'
2
+
3
+ module Adva
4
+ class Static
5
+ module Rack
6
+ class Watch
7
+ autoload :Handler, 'adva/static/watch/handler'
8
+
9
+ include Request
10
+
11
+ attr_reader :app, :dir, :watch
12
+
13
+ delegate :call, :to => :app
14
+
15
+ def initialize(app, options = {}, &block)
16
+ @app = app
17
+ @dir = Pathname.new(options[:dir] || File.expand_path('import'))
18
+ dir.mkpath
19
+ run!
20
+ end
21
+
22
+ def update(path, event_type = nil)
23
+ Adva.out.puts "\n#{event_type}: #{path}"
24
+ import = Adva::Static::Import.new(:source => dir)
25
+ request = import.request_for(path)
26
+ status, headers, response = self.request('POST', request.path, request.params)
27
+ get(path) if !request.destroy? && status == 302
28
+ rescue Exception => e
29
+ Adva.out.puts e.message
30
+ e.backtrace.each { |line| puts Adva.out.line }
31
+ end
32
+
33
+ def get(path)
34
+ import = Adva::Static::Import.new(:source => dir)
35
+ request = import.request_for(path)
36
+ self.request('GET', request.public_path, STORE_HEADER => true)
37
+ rescue Exception => e
38
+ Adva.out.puts e.message
39
+ e.backtrace.each { |line| Adva.out.puts line }
40
+ end
41
+
42
+ protected
43
+
44
+ def run!
45
+ @watch = fork { watch!(Adva.out) }
46
+ at_exit { kill_watch }
47
+ end
48
+
49
+ def watch!(out)
50
+ Adva.out.puts "watching #{dir} for changes"
51
+ Dir.chdir(dir)
52
+ handler.listen
53
+ rescue SignalException, SystemExit
54
+ rescue Exception => e
55
+ p e
56
+ e.backtrace.each { |line| puts line }
57
+ end
58
+
59
+ def trap_interrupt
60
+ Signal.trap('INT') do
61
+ if Time.now - @interrupt < 1
62
+ exit
63
+ else
64
+ STDERR.puts "\nReloading watched paths ... interrupt again to exit."
65
+ handler.refresh(watched_paths)
66
+ end
67
+ @interrupt = Time.now
68
+ end
69
+ @interrupt = Time.now
70
+ end
71
+
72
+ def handler
73
+ @handler ||= Adva::Static::Watch::Handler.new(self, dir.join("**/*.{#{Import::Source::TYPES.join(',')}}"))
74
+ end
75
+
76
+ def kill_watch
77
+ Process.kill('TERM', watch)
78
+ end
79
+
80
+ def watched_paths
81
+ paths = Dir[dir.join('**/*')].map {|path| Pathname(path).expand_path }
82
+ paths << Pathname.new(__FILE__) if paths.empty?
83
+ paths
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,15 @@
1
+ require 'adva'
2
+
3
+ module Adva
4
+ class Static
5
+ module Rack
6
+ PURGE_HEADER = 'rack-cache.purge'
7
+ STORE_HEADER = 'rack-static.store'
8
+
9
+ autoload :Request, 'adva/static/rack/request'
10
+ autoload :Export, 'adva/static/rack/export'
11
+ autoload :Static, 'adva/static/rack/static'
12
+ autoload :Watch, 'adva/static/rack/watch'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,68 @@
1
+ require 'core_ext/ruby/kernel/silence_stream'
2
+
3
+ module Adva
4
+ class Static
5
+ class Setup
6
+ attr_reader :app, :root, :source, :target, :host, :title, :remote
7
+
8
+ def initialize(options)
9
+ @app = options[:app] || Rails.application
10
+ @root = Pathname.new(options[:root] || Dir.pwd)
11
+ @source = root.join(options[:source] || 'import')
12
+ @target = root.join(options[:target] || 'export')
13
+ @remote = options[:remote]
14
+ @host = options[:host] || 'example.org'
15
+ @title = options[:title] || host
16
+
17
+ Adva.out = StringIO.new('')
18
+ end
19
+
20
+ def run
21
+ setup_directories
22
+ initial_import_and_export
23
+ setup_source_repository
24
+ setup_export_repository
25
+ end
26
+
27
+ def setup_directories
28
+ source.mkdir rescue Errno::EEXIST
29
+ target.mkdir rescue Errno::EEXIST
30
+ site = source.join('site.yml')
31
+ File.open(site, 'w+') { |f| f.write(YAML.dump(:host => host, :title => title)) } unless site.exist?
32
+ end
33
+
34
+ def initial_import_and_export
35
+ Import.new(:source => source).run
36
+ Export.new(app, :target => target).run
37
+ end
38
+
39
+ def setup_source_repository
40
+ root.join('.gitignore').rmtree rescue Errno::ENOENT
41
+ root.join('.git').rmtree rescue Errno::ENOENT
42
+
43
+ File.open(root.join('.gitignore'), 'w+') { |f| f.write('export') }
44
+
45
+ Dir.chdir(root) do
46
+ `git init`
47
+ `git add .`
48
+ `git commit -am '#{host} source'`
49
+ `git branch source`
50
+ `git checkout --quiet source`
51
+ `git branch -D master`
52
+ `git remote add origin #{remote} -t source` if remote
53
+ end
54
+ end
55
+
56
+ def setup_export_repository
57
+ root.join('export/.git').rmtree rescue Errno::ENOENT
58
+
59
+ Dir.chdir(target) do
60
+ `git init`
61
+ `git add .`
62
+ `git commit -am '#{host} export'`
63
+ `git remote add origin #{remote} -t master` if remote
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,57 @@
1
+ require 'observer'
2
+ require 'watchr'
3
+ require 'watchr/event_handlers/portable'
4
+
5
+ module Adva
6
+ class Static
7
+ module Watch
8
+ class Handler
9
+ include Observable
10
+
11
+ def initialize(observable, pattern)
12
+ add_observer(observable)
13
+ @pattern = pattern
14
+ @current = Dir[pattern]
15
+ @mtime = Time.now
16
+ end
17
+
18
+ def listen
19
+ loop { trigger; sleep(0.5) }
20
+ end
21
+
22
+ def trigger
23
+ events.each do |path, event|
24
+ changed(true)
25
+ notify_observers(path, event)
26
+ end
27
+ end
28
+
29
+ protected
30
+
31
+ def events
32
+ @last = @current.dup
33
+ @current = Dir[@pattern]
34
+ deleted + created + modified
35
+ end
36
+
37
+ def modified
38
+ (@current & @last).each do |path|
39
+ mtime = File.mtime(path)
40
+ if mtime > @mtime
41
+ @mtime = mtime
42
+ return [[path, :modified]]
43
+ end
44
+ end && []
45
+ end
46
+
47
+ def created
48
+ (@current - @last).map { |path| [path, :created] }
49
+ end
50
+
51
+ def deleted
52
+ (@last - @current).map { |path| [path, :deleted] }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,7 @@
1
+ module Adva
2
+ class Static
3
+ module Watch
4
+ autoload :Handler, 'adva/static/watch/handler'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ require 'adva'
2
+
3
+ module Adva
4
+ class Static < ::Rails::Engine
5
+ autoload :Export, 'adva/static/export'
6
+ autoload :Import, 'adva/static/import'
7
+ autoload :Watch, 'adva/static/watch'
8
+ autoload :Rack, 'adva/static/rack'
9
+ autoload :Setup, 'adva/static/setup'
10
+
11
+ include Adva::Engine
12
+ end
13
+ end
@@ -0,0 +1,73 @@
1
+ require 'thor'
2
+ require 'thor/group'
3
+ require 'patches/thor/core_ext/hash'
4
+ require 'patches/thor/group/symbolized_options'
5
+
6
+ module Adva
7
+ module Tasks
8
+ class Static
9
+ class Setup < Thor::Group
10
+ namespace 'adva:static:setup'
11
+ desc 'Setup a static version of your site'
12
+ class_option :source, :required => false, :banner => 'source directory (defaults to import)'
13
+ class_option :target, :required => false, :banner => 'source directory (defaults to export)'
14
+ class_option :host, :required => false, :banner => 'hostname of your site (defaults to example.org)'
15
+ class_option :title, :required => false, :banner => 'title of your site (defaults to the hostname)'
16
+ class_option :remote, :required => false, :banner => 'github repository url (defaults to none)'
17
+
18
+ def export
19
+ require 'config/environment'
20
+ Adva::Static::Setup.new(symbolized_options).run
21
+ end
22
+ end
23
+
24
+ class Import < Thor::Group
25
+ namespace 'adva:static:import'
26
+ desc 'Import a site from a directory'
27
+ class_option :source, :required => false
28
+
29
+ def import
30
+ require 'config/environment'
31
+ Adva::Static::Import.new(symbolized_options).run
32
+ end
33
+ end
34
+
35
+ class Export < Thor::Group
36
+ namespace 'adva:static:export'
37
+ desc 'Export a static version of a site'
38
+ class_option :target, :required => false
39
+
40
+ def export
41
+ require 'config/environment'
42
+ Adva::Static::Export.new(Rails.application, symbolized_options).run
43
+ end
44
+ end
45
+
46
+ class Update < Thor::Group
47
+ namespace 'adva:static:update'
48
+ desc 'Import and export a static version of a site'
49
+ class_option :source, :required => false
50
+ class_option :target, :required => false
51
+
52
+ def export
53
+ require 'config/environment'
54
+ Adva::Static::Import.new(symbolized_options).run
55
+ Adva::Static::Export.new(Rails.application, symbolized_options).run
56
+ end
57
+ end
58
+
59
+ class Server < Thor::Group
60
+ namespace 'adva:static:server'
61
+ desc 'Start the adva:static server and watcher'
62
+ class_option :root, :required => false, :default => 'export'
63
+
64
+ def server
65
+ ARGV.shift
66
+ Dir.chdir(symbolized_options[:root])
67
+ require "rack"
68
+ ::Rack::Server.start
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1 @@
1
+ require 'adva/static'