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,45 @@
1
+ require 'rack'
2
+
3
+ module Adva
4
+ class Static
5
+ class Export
6
+ class Page
7
+ URL_ATTRIBUTES = {
8
+ '//a[@href]' => 'href',
9
+ '//script[@src]' => 'src',
10
+ '//link[@rel="stylesheet"]' => 'href'
11
+ }
12
+
13
+ attr_reader :url, :response
14
+
15
+ def initialize(url, response)
16
+ @url = Path.new(url)
17
+ @response = response
18
+ end
19
+
20
+ def urls
21
+ URL_ATTRIBUTES.inject([]) do |urls, (xpath, name)|
22
+ urls += dom.xpath(xpath).map { |node| Path.new(node.attributes[name]) }
23
+ end
24
+ end
25
+
26
+ def body
27
+ @body ||= case response
28
+ when ActionDispatch::Response
29
+ response.body
30
+ when ::Rack::File
31
+ File.read(response.path)
32
+ else
33
+ response.to_s
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ def dom
40
+ @dom ||= Nokogiri::HTML(body)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,49 @@
1
+ require 'uri'
2
+
3
+ module Adva
4
+ class Static
5
+ class Export
6
+ class Path < String
7
+ attr_reader :host
8
+
9
+ def initialize(path)
10
+ @host = URI.parse(path.to_s).host rescue 'invalid.host'
11
+ path = normalize_path(path)
12
+ super
13
+ end
14
+
15
+ def filename
16
+ @filename ||= normalize_filename(self)
17
+ end
18
+
19
+ def extname
20
+ @extname ||= File.extname(self)
21
+ end
22
+
23
+ def html?
24
+ extname.blank? || extname == '.html'
25
+ end
26
+
27
+ def remote?
28
+ host.present?
29
+ end
30
+
31
+ protected
32
+
33
+ def normalize_path(path)
34
+ path = URI.parse(path.to_s).path || '/' rescue '/' # extract path
35
+ path = path[0..-2] if path[-1, 1] == '/' # remove trailing slash
36
+ path = "/#{path}" unless path[0, 1] == '/' # add leading slash
37
+ path
38
+ end
39
+
40
+ def normalize_filename(path)
41
+ path = path[1..-1] if path[0, 1] == '/' # remove leading slash
42
+ path = 'index' if path.empty? # use 'index' instead of empty paths
43
+ path = (html? ? "#{path.gsub(extname, '')}.html" : path) # add .html extension if necessary
44
+ path
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,27 @@
1
+ module Adva
2
+ class Static
3
+ class Export
4
+ class Queue < Array
5
+ def push(*elements)
6
+ elements = Array(elements).flatten.uniq
7
+ elements.reject! { |element| seen?(element) }
8
+ seen(elements)
9
+ super
10
+ end
11
+
12
+ def seen?(element)
13
+ log.include?(element)
14
+ end
15
+
16
+ def seen(elements)
17
+ @log = log.concat(elements)
18
+ log.uniq!
19
+ end
20
+
21
+ def log
22
+ @log ||= []
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ require 'fileutils'
2
+
3
+ module Adva
4
+ class Static
5
+ class Export
6
+ class Store
7
+ attr_reader :dir
8
+
9
+ def initialize(dir)
10
+ @dir = Pathname.new(dir.to_s)
11
+ FileUtils.mkdir_p(dir)
12
+ end
13
+
14
+ def exists?(path)
15
+ File.exists?(dir.join(path.filename))
16
+ end
17
+
18
+ def write(path, body)
19
+ path = dir.join(path.filename)
20
+ FileUtils.mkdir_p(File.dirname(path))
21
+ File.open(path, 'w+') { |f| f.write(body) }
22
+ end
23
+
24
+ def purge(path)
25
+ dir.join(path.filename).delete rescue Errno::ENOENT
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,14 @@
1
+ Dir.chdir('..') until File.exists?('config/environment.rb')
2
+
3
+ require 'config/environment.rb'
4
+
5
+ Rails::Application.configure do
6
+ ActionController::Base.allow_forgery_protection = false
7
+ end
8
+
9
+ use Adva::Static::Rack::Watch
10
+ use Adva::Static::Rack::Export
11
+ use Adva::Static::Rack::Static, ::File.expand_path('../export', __FILE__)
12
+
13
+ puts 'listening.'
14
+ run Rails.application
@@ -0,0 +1,104 @@
1
+ require 'nokogiri'
2
+ require 'uri'
3
+ require 'benchmark'
4
+
5
+ module Adva
6
+ class Static
7
+ class Export
8
+ autoload :Page, 'adva/static/export/page'
9
+ autoload :Path, 'adva/static/export/path'
10
+ autoload :Queue, 'adva/static/export/queue'
11
+ autoload :Store, 'adva/static/export/store'
12
+
13
+ attr_reader :app, :queue, :store, :options
14
+
15
+ DEFAULT_OPTIONS = {
16
+ :source => "#{Dir.pwd}/public",
17
+ :target => "#{Dir.pwd}/export"
18
+ }
19
+
20
+ def initialize(app, options = {})
21
+ @options = options.reverse_merge!(DEFAULT_OPTIONS)
22
+
23
+ @app = app
24
+ @store = Store.new(target)
25
+ @queue = Queue.new
26
+
27
+ queue.push(options[:queue] || Path.new('/'))
28
+
29
+ FileUtils.rm_r(Dir[target.join('*')])
30
+ end
31
+
32
+ def run
33
+ configure
34
+ copy_assets
35
+ process(queue.shift) until queue.empty?
36
+ end
37
+
38
+ protected
39
+
40
+ def source
41
+ @source ||= Pathname.new(options[:source])
42
+ end
43
+
44
+ def target
45
+ @target ||= Pathname.new(options[:target])
46
+ end
47
+
48
+ def copy_assets
49
+ %w(images javascripts stylesheets).each do |dir|
50
+ FileUtils.cp_r(source.join(dir), target.join(dir)) if source.join(dir).exist?
51
+ end
52
+ end
53
+
54
+ def process(path)
55
+ if page = get(path)
56
+ store.write(path, page.body)
57
+ enqueue_urls(page) if path.html?
58
+ end
59
+ end
60
+
61
+ def get(path)
62
+ result = nil
63
+ bench = Benchmark.measure do
64
+ result = app.call(env_for(path))
65
+ result = follow_redirects(result)
66
+ end
67
+
68
+ status, headers, response = result
69
+ if status == 200
70
+ Adva.out.puts "#{bench.total.to_s[0..3]}s: exporting #{path}"
71
+ Page.new(path, headers['X-Sendfile'] ? File.read(headers['X-Sendfile']) : response)
72
+ else
73
+ Adva.out.puts "can not export #{path} (status: #{status})"
74
+ end
75
+ end
76
+
77
+ def follow_redirects(response)
78
+ response = app.call(env_for(response[1]['Location'])) while redirect?(response[0])
79
+ response
80
+ end
81
+
82
+ def redirect?(status)
83
+ status == 301
84
+ end
85
+
86
+ def env_for(path)
87
+ site = Site.first || raise('could not find any site') # TODO make this a cmd line arg or options
88
+ name, port = site.host.split(':')
89
+ ::Rack::MockRequest.env_for(path).merge('SERVER_NAME' => name,'SERVER_PORT' => port || '80')
90
+ end
91
+
92
+ def enqueue_urls(page)
93
+ queue.push(page.urls.reject { |path| path.remote? || store.exists?(path) }.uniq)
94
+ end
95
+
96
+ def configure
97
+ config = Path.new('config.ru')
98
+ unless store.exists?(config)
99
+ store.write(config, File.read(File.expand_path('../export/templates/config.ru', __FILE__)))
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,58 @@
1
+ module Adva
2
+ class Static
3
+ class Import
4
+ module Format
5
+ def self.for(path)
6
+ name = File.extname(path).gsub('.', '').camelize
7
+ const_get(name).new(path) if name.present?
8
+ end
9
+
10
+ class Base
11
+ attr_reader :path
12
+
13
+ def initialize(path)
14
+ @path = path
15
+ end
16
+
17
+ def load(target)
18
+ data.each do |name, value|
19
+ define_attribute(target, name) if define_attribute?(target, name)
20
+ target.instance_variable_set(:"@#{name}", value)
21
+ end if data.is_a?(Hash)
22
+ end
23
+
24
+ def define_attribute?(target, name)
25
+ !target.attribute_name?(name) && target.column_name?(name)
26
+ end
27
+
28
+ def define_attribute(target, name)
29
+ target.attribute_names << name
30
+ target.attribute_names.uniq!
31
+ target.class.send(:attr_reader, name) unless target.respond_to?(name)
32
+ end
33
+ end
34
+
35
+ class Yml < Base
36
+ def data
37
+ @data ||= YAML.load_file(path)
38
+ end
39
+ end
40
+
41
+ class Jekyll < Base
42
+ def data
43
+ @data ||= begin
44
+ file =~ /^(---\s*\n.*?\n?)^---\s*$\n?(.*)/m
45
+ data = YAML.load($1) rescue {}
46
+ data.merge!(:body => $2) if $2
47
+ data
48
+ end
49
+ end
50
+
51
+ def file
52
+ @file ||= File.read(path)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,78 @@
1
+ require 'core_ext/ruby/array/flatten_once'
2
+
3
+ module Adva
4
+ class Static
5
+ class Import
6
+ module Model
7
+ class Base
8
+ attr_reader :source, :attribute_names
9
+
10
+ def initialize(source)
11
+ @source = source
12
+ load
13
+ end
14
+
15
+ def attributes
16
+ attributes = attribute_names.map { |name| [name, self.send(name)] unless self.send(name).nil? }
17
+ attributes = Hash[*attributes.compact.flatten_once]
18
+ record && record.id ? attributes.merge(:id => record.id.to_s) : attributes
19
+ end
20
+
21
+ def attribute_name?(name)
22
+ attribute_names.include?(name.to_sym)
23
+ end
24
+
25
+ def column_name?(name)
26
+ model.column_names.include?(name.to_s)
27
+ end
28
+
29
+ def updated_record
30
+ record.tap { |record| record.attributes = attributes }
31
+ end
32
+
33
+ def model
34
+ self.class.name.demodulize.constantize
35
+ end
36
+
37
+ def site_id
38
+ site.id.to_s
39
+ end
40
+
41
+ def slug
42
+ source.basename
43
+ end
44
+
45
+ def path
46
+ source.path
47
+ end
48
+
49
+ def body
50
+ @body || ''
51
+ end
52
+
53
+ def updated_at
54
+ source.mtime
55
+ end
56
+
57
+ def loadable
58
+ @loadable ||= source.full_path
59
+ end
60
+
61
+ def load
62
+ if loadable.exist?
63
+ format = Format.for(loadable) and format.load(self)
64
+ end
65
+ end
66
+
67
+ def ==(other)
68
+ source == other
69
+ end
70
+
71
+ def <=>(other)
72
+ source <=> other.source
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,33 @@
1
+ module Adva
2
+ class Static
3
+ class Import
4
+ module Model
5
+ class Blog < Section
6
+ class << self
7
+ def recognize(sources)
8
+ return [] if sources.blank?
9
+
10
+ sources = Array(sources)
11
+ posts = sources.select { |source| Post.permalink?(source) }
12
+ posts = sources.map(&:directory).map(&:files).flatten.select { |s| Post.permalink?(s) } if posts.blank?
13
+
14
+ blogs = posts.map { |post| Post.new(post).section_source }.flatten.uniq
15
+ blogs = blogs.map { |blog| sources.detect { |source| blog.path == source.path } || blog }
16
+
17
+ sources.replace(sources - blogs - posts.map(&:self_and_parents).flatten)
18
+ blogs.map { |source| new(source) }
19
+ end
20
+ end
21
+
22
+ def attribute_names
23
+ @attribute_names ||= super + [:posts_attributes]
24
+ end
25
+
26
+ def posts_attributes
27
+ Post.recognize(source.files).map(&:attributes)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ module Adva
2
+ class Static
3
+ class Import
4
+ module Model
5
+ class Page < Section
6
+ PATTERN = %r([\w-]+\.(#{Source::TYPES.join('|')})$)
7
+
8
+ class << self
9
+ def recognize(sources)
10
+ return [] if sources.blank?
11
+
12
+ pages = sources.select { |source| source.to_s =~ PATTERN }
13
+ sources.replace(sources - pages)
14
+
15
+ pages = pages.map { |source| source.self_and_parents.map(&:find_or_self) }.flatten.uniq
16
+ pages = pages.map { |source| new(source) }
17
+ pages
18
+ end
19
+ end
20
+
21
+ def attribute_names
22
+ @attribute_names ||= super + [:article_attributes]
23
+ end
24
+
25
+ def article_attributes
26
+ attributes = { :title => name, :body => body }
27
+ record.persisted? ? attributes.merge(:id => record.article.id.to_s) : attributes
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,78 @@
1
+ module Adva
2
+ class Static
3
+ class Import
4
+ module Model
5
+ class Post < Base
6
+ PERMALINK = %r((?:^|/)(\d{4})(?:\-|\/)(\d{1,2})(?:\-|\/)(\d{1,2})(?:\-|\/)(.*)$)
7
+
8
+ class << self
9
+ def recognize(sources)
10
+ posts = sources.select { |source| source.path =~ PERMALINK }
11
+ sources.replace(sources - posts.map(&:self_and_parents).flatten)
12
+ posts.map { |post| new(post) }
13
+ end
14
+
15
+ def permalink?(path)
16
+ path.to_s =~ PERMALINK
17
+ end
18
+
19
+ def strip_permalink(source)
20
+ Source.new(source.to_s.gsub(Post::PERMALINK, ''), source.root)
21
+ end
22
+ end
23
+
24
+ def attribute_names
25
+ @attribute_names ||= [:site_id, :section_id, :title, :body, :created_at] # TODO created_at should be published_at
26
+ end
27
+
28
+ def record
29
+ @record ||= section.posts.by_permalink(*permalink).all.first || section.posts.build
30
+ end
31
+
32
+ def site_id
33
+ section.site_id.to_s
34
+ end
35
+
36
+ def section
37
+ @section ||= Blog.new(section_source).record
38
+ end
39
+
40
+ def section_id
41
+ section.id.to_s
42
+ end
43
+
44
+ def section_source
45
+ @section_source ||= begin
46
+ source = self.class.strip_permalink(self.source)
47
+ if source.path.present?
48
+ source.find_or_self
49
+ else
50
+ Source.new(source.join('index'), source.root).find_or_self
51
+ end
52
+ end
53
+ end
54
+
55
+ def slug
56
+ @slug ||= SimpleSlugs::Slug.new(title).to_s
57
+ end
58
+
59
+ def title
60
+ @title ||= path_tokens.last.titleize
61
+ end
62
+
63
+ def permalink
64
+ @permalink ||= path_tokens.to_a[0..-2] << slug
65
+ end
66
+
67
+ def path_tokens
68
+ @path_tokens ||= source.to_s.gsub(/\.\w+$/, '').match(PERMALINK).to_a[1..-1]
69
+ end
70
+
71
+ def created_at
72
+ @created_at ||= DateTime.civil(*permalink[0..-2].map(&:to_i))
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,51 @@
1
+ module Adva
2
+ class Static
3
+ class Import
4
+ module Model
5
+ class Section < Base
6
+ class << self
7
+ def types
8
+ [Blog, Page]
9
+ end
10
+
11
+ def recognize(sources)
12
+ types.map { |type| type.recognize(sources) }.flatten.compact.sort
13
+ end
14
+ end
15
+
16
+ def attribute_names
17
+ @attribute_names ||= [:site_id, :type, :name, :slug, :path]
18
+ end
19
+
20
+ def record
21
+ @record ||= site.send(model.name.underscore.pluralize).find_or_initialize_by_path(path)
22
+ end
23
+
24
+ def site
25
+ @site ||= Site.new(source.root).record
26
+ end
27
+
28
+ def type
29
+ model.name
30
+ end
31
+
32
+ def name
33
+ @name ||= source.root? ? 'Home' : source.basename.titleize
34
+ end
35
+
36
+ def slug
37
+ @slug ||= source.root? ? SimpleSlugs::Slug.new(name) : super
38
+ end
39
+
40
+ def path
41
+ @path ||= source.root? ? slug : super
42
+ end
43
+
44
+ def loadable
45
+ @loadable ||= source.root? ? Source.new('index', source.root).find_or_self.full_path : source.full_path
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,59 @@
1
+ require 'site'
2
+
3
+ module Adva
4
+ class Static
5
+ class Import
6
+ module Model
7
+ class Site < Base
8
+ class << self
9
+ def recognize(sources)
10
+ sources.map { |source| new(sources.delete(source).root) if source.path == 'site' }.compact
11
+ end
12
+ end
13
+
14
+ def initialize(root)
15
+ super(Source.new('', root))
16
+ end
17
+
18
+ def attribute_names
19
+ @attribute_names ||= [:account, :host, :name, :title, :sections_attributes]
20
+ end
21
+
22
+ def record
23
+ @record ||= model.find_or_initialize_by_host(host)
24
+ end
25
+
26
+ def host
27
+ @host ||= File.basename(source.root)
28
+ end
29
+
30
+ def name
31
+ @name ||= host
32
+ end
33
+
34
+ def title
35
+ @title ||= name
36
+ end
37
+
38
+ def account
39
+ @account ||= ::Account.first || ::Account.new
40
+ end
41
+
42
+ def sections_attributes
43
+ sections.map(&:attributes)
44
+ end
45
+
46
+ def sections
47
+ @sections ||= Section.recognize(source.files).tap do |sections|
48
+ sections << Page.new(Source.new('index', source.root).find_or_self) if sections.empty?
49
+ end
50
+ end
51
+
52
+ def loadable
53
+ @loadable ||= Source.new('site', source.root).find_or_self.full_path
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,21 @@
1
+ module Adva
2
+ class Static
3
+ class Import
4
+ module Model
5
+ autoload :Base, 'adva/static/import/model/base'
6
+ autoload :Blog, 'adva/static/import/model/blog'
7
+ autoload :Page, 'adva/static/import/model/page'
8
+ autoload :Post, 'adva/static/import/model/post'
9
+ autoload :Section, 'adva/static/import/model/section'
10
+ autoload :Site, 'adva/static/import/model/site'
11
+
12
+ class << self
13
+ def recognize(sources)
14
+ types = [Site, Post, Section]
15
+ types.map { |type| type.recognize(sources) }.flatten.compact.sort
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end