adva-static 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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