skypager 0.0.2
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.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/Rakefile +21 -0
- data/bin/skypager +17 -0
- data/examples/.gitignore +4 -0
- data/examples/blog-site/.gitignore +18 -0
- data/examples/blog-site/.pryrc +4 -0
- data/examples/blog-site/Gemfile +8 -0
- data/examples/blog-site/config.rb +17 -0
- data/examples/blog-site/data/dropbox.json +1 -0
- data/examples/blog-site/source/images/background.png +0 -0
- data/examples/blog-site/source/images/middleman.png +0 -0
- data/examples/blog-site/source/index.html.erb +10 -0
- data/examples/blog-site/source/javascripts/all.js +1 -0
- data/examples/blog-site/source/layouts/layout.erb +19 -0
- data/examples/blog-site/source/posts/introduction-to-skypager.html.md +23 -0
- data/examples/blog-site/source/posts/skypager-and-dnsimple-and-amazon-web-services-combo.html.md +9 -0
- data/examples/blog-site/source/stylesheets/all.css +55 -0
- data/examples/blog-site/source/stylesheets/normalize.css +375 -0
- data/examples/gallery-site/.gitignore +18 -0
- data/examples/gallery-site/.pryrc +4 -0
- data/examples/gallery-site/Gemfile +11 -0
- data/examples/gallery-site/config.rb +38 -0
- data/examples/gallery-site/data/dropbox.json +1 -0
- data/examples/gallery-site/data/galleries.json +1 -0
- data/examples/gallery-site/source/gallery.html.erb +7 -0
- data/examples/gallery-site/source/images/background.png +0 -0
- data/examples/gallery-site/source/images/galleries/cristian-gallery-1/001.jpg +0 -0
- data/examples/gallery-site/source/images/galleries/cristian-gallery-1/002.jpg +0 -0
- data/examples/gallery-site/source/images/galleries/cristian-gallery-1/003.jpg +0 -0
- data/examples/gallery-site/source/images/galleries/cristian-gallery-1/004.jpg +0 -0
- data/examples/gallery-site/source/images/galleries/luca-gallery-1/001.jpg +0 -0
- data/examples/gallery-site/source/images/galleries/luca-gallery-1/002.JPG +0 -0
- data/examples/gallery-site/source/images/galleries/luca-gallery-1/003.jpg +0 -0
- data/examples/gallery-site/source/images/galleries/luca-gallery-1/004.JPG +0 -0
- data/examples/gallery-site/source/images/middleman.png +0 -0
- data/examples/gallery-site/source/index.html.erb +10 -0
- data/examples/gallery-site/source/javascripts/all.js +1 -0
- data/examples/gallery-site/source/layouts/layout.erb +20 -0
- data/examples/gallery-site/source/stylesheets/all.css +0 -0
- data/examples/gallery-site/source/stylesheets/normalize.css +375 -0
- data/examples/gallery-site/source/tutorial.md +151 -0
- data/lib/skypager.rb +92 -0
- data/lib/skypager/build_server.rb +17 -0
- data/lib/skypager/cli/commands/config.rb +58 -0
- data/lib/skypager/cli/commands/create.rb +98 -0
- data/lib/skypager/cli/commands/deploy.rb +30 -0
- data/lib/skypager/cli/commands/edit.rb +32 -0
- data/lib/skypager/cli/commands/list.rb +12 -0
- data/lib/skypager/cli/commands/setup.rb +124 -0
- data/lib/skypager/cli/commands/sync.rb +18 -0
- data/lib/skypager/configuration.rb +173 -0
- data/lib/skypager/data.rb +8 -0
- data/lib/skypager/data/excel_spreadsheet.rb +8 -0
- data/lib/skypager/data/google_spreadsheet.rb +225 -0
- data/lib/skypager/data/request.rb +12 -0
- data/lib/skypager/data/source.rb +171 -0
- data/lib/skypager/data/source_routes_proxy.rb +30 -0
- data/lib/skypager/dns.rb +65 -0
- data/lib/skypager/extension.rb +203 -0
- data/lib/skypager/middleman/commands/data.rb +0 -0
- data/lib/skypager/middleman/commands/deploy.rb +0 -0
- data/lib/skypager/middleman/commands/sync.rb +0 -0
- data/lib/skypager/site.rb +208 -0
- data/lib/skypager/sync.rb +23 -0
- data/lib/skypager/sync/amazon.rb +171 -0
- data/lib/skypager/sync/dropbox.rb +173 -0
- data/lib/skypager/sync/dropbox/delta.rb +67 -0
- data/lib/skypager/sync/folder.rb +235 -0
- data/lib/skypager/sync/google.rb +143 -0
- data/lib/skypager/tar.rb +77 -0
- data/lib/skypager/version.rb +3 -0
- data/skypager.gemspec +40 -0
- data/spec/lib/skypager/configuration_spec.rb +5 -0
- data/spec/lib/skypager/data_spec.rb +5 -0
- data/spec/lib/skypager/site_spec.rb +5 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/json_helper.rb +7 -0
- metadata +383 -0
@@ -0,0 +1,171 @@
|
|
1
|
+
module Skypager
|
2
|
+
module Data
|
3
|
+
class Source
|
4
|
+
attr_reader :options, :name
|
5
|
+
attr_accessor :raw, :processed, :format, :scopes, :slug_column, :refreshed_at, :path
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :required_options
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.requires *args
|
12
|
+
self.required_options = args
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(name, options={})
|
16
|
+
@name ||= name
|
17
|
+
@options ||= options
|
18
|
+
@format ||= options.fetch(:format, :json)
|
19
|
+
@path ||= options.fetch(:path) { Pathname(Dir.pwd()) }
|
20
|
+
|
21
|
+
@slug_column = options.fetch(:slug_column, :_id)
|
22
|
+
|
23
|
+
ensure_valid_options!
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
data.to_json
|
28
|
+
end
|
29
|
+
|
30
|
+
# defines a scope for the records in this data source
|
31
|
+
# a scope is a named filter, implemented in the form of a block
|
32
|
+
# which is passed each record. if the block returns true, it returns
|
33
|
+
# the record:
|
34
|
+
#
|
35
|
+
# Example:
|
36
|
+
#
|
37
|
+
# data_source(:galleries) do
|
38
|
+
# scope :active, -> {|record| record.state == "active" }
|
39
|
+
# end
|
40
|
+
def scope(*args, block)
|
41
|
+
name = args.first
|
42
|
+
(self.scopes ||= {})[name.to_sym] = block
|
43
|
+
end
|
44
|
+
|
45
|
+
def has_scope?(scope_name)
|
46
|
+
scope_name && (self.scopes ||= {}).key?(scope_name.to_sym)
|
47
|
+
end
|
48
|
+
|
49
|
+
# compute properties takes the raw data of each record
|
50
|
+
# and sets additional properties on the records which may
|
51
|
+
# not be persited in the data source
|
52
|
+
def compute_properties
|
53
|
+
self.processed && self.processed.map! do |row|
|
54
|
+
if slug_column && row.respond_to?(slug_column)
|
55
|
+
row.slug = row.send(slug_column).to_s.parameterize
|
56
|
+
end
|
57
|
+
|
58
|
+
row
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# makes sure that the required options for this data source
|
63
|
+
# are passed for any instance of the data source
|
64
|
+
def ensure_valid_options!
|
65
|
+
missing_options = (Array(self.class.required_options) - options.keys.map(&:to_sym))
|
66
|
+
|
67
|
+
missing_options.reject! do |key|
|
68
|
+
respond_to?(key) && send(key).present?
|
69
|
+
end
|
70
|
+
|
71
|
+
if missing_options.length > 0
|
72
|
+
raise 'Error: failure to supply the following options:' + missing_options.map(&:to_s).join(",")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def select(&block)
|
77
|
+
data.select(&block)
|
78
|
+
end
|
79
|
+
|
80
|
+
def refresh
|
81
|
+
fetch
|
82
|
+
process
|
83
|
+
self.refreshed_at = Time.now.to_i
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
def refresh_if_stale?
|
88
|
+
refresh! if stale?
|
89
|
+
end
|
90
|
+
|
91
|
+
# A data source is stale if it has been populated
|
92
|
+
# and the age is greater than the max age we allow.
|
93
|
+
def stale?
|
94
|
+
!need_to_refresh? && (age > max_age)
|
95
|
+
end
|
96
|
+
|
97
|
+
def max_age
|
98
|
+
max = ENV['MAX_DATA_SOURCE_AGE']
|
99
|
+
(max && max.to_i) || 120
|
100
|
+
end
|
101
|
+
|
102
|
+
# how long since this data source has been refreshed?
|
103
|
+
def age
|
104
|
+
Time.now.to_i - refreshed_at.to_i
|
105
|
+
end
|
106
|
+
|
107
|
+
def data
|
108
|
+
refresh if need_to_refresh?
|
109
|
+
processed
|
110
|
+
end
|
111
|
+
|
112
|
+
def refresh!
|
113
|
+
refresh
|
114
|
+
save_to_disk
|
115
|
+
end
|
116
|
+
|
117
|
+
def need_to_refresh?
|
118
|
+
!(@fetched && @_processed)
|
119
|
+
end
|
120
|
+
|
121
|
+
def fetch
|
122
|
+
@fetched = true
|
123
|
+
self.raw = []
|
124
|
+
end
|
125
|
+
|
126
|
+
def preprocess
|
127
|
+
self.raw.dup
|
128
|
+
end
|
129
|
+
|
130
|
+
def process
|
131
|
+
@_processed = true
|
132
|
+
self.processed = preprocess
|
133
|
+
# set_id
|
134
|
+
compute_properties
|
135
|
+
self.processed
|
136
|
+
end
|
137
|
+
|
138
|
+
def refreshed_at
|
139
|
+
return @refreshed_at if @refreshed_at.to_i > 0
|
140
|
+
|
141
|
+
if path_to_file.exist?
|
142
|
+
@refreshed_at = FileUtils.mtime(path.join(file)).to_i
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def save_to_disk
|
147
|
+
unless path_to_file.dirname.exist?
|
148
|
+
FileUtils.mkdir(path_to_file.dirname)
|
149
|
+
end
|
150
|
+
|
151
|
+
path_to_file.open('w+') {|fh| fh.write(to_s) }
|
152
|
+
end
|
153
|
+
|
154
|
+
def persisted?
|
155
|
+
path_to_file && path_to_file.exist?
|
156
|
+
end
|
157
|
+
|
158
|
+
def file
|
159
|
+
@file ||= name.parameterize if name.respond_to?(:parameterize)
|
160
|
+
@file = "#{@file}.json" unless @file.match(/\.json/i)
|
161
|
+
@file
|
162
|
+
end
|
163
|
+
|
164
|
+
def path_to_file
|
165
|
+
Pathname(path).join("#{ file }")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'uri_template'
|
2
|
+
|
3
|
+
module Skypager
|
4
|
+
module Data
|
5
|
+
class SourceRoutesProxy
|
6
|
+
def initialize(app, data_source, options={})
|
7
|
+
template = URITemplate.new(:colon, options[:url])
|
8
|
+
|
9
|
+
data = data_source.data
|
10
|
+
|
11
|
+
# If we're passed a scope that means we should
|
12
|
+
# limit the set of records we are mapping to by the
|
13
|
+
# scope defined in this data source
|
14
|
+
if options[:scope]
|
15
|
+
tester = data_source.scopes[options[:scope]]
|
16
|
+
end
|
17
|
+
|
18
|
+
to = options[:template] || options[:to]
|
19
|
+
|
20
|
+
as = options.fetch(:as, :current_record)
|
21
|
+
|
22
|
+
data.each do |row|
|
23
|
+
url = template.expand(row.to_hash)
|
24
|
+
app.proxy(url, to, :locals => {as => row}, :ignore => true)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/skypager/dns.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
module Skypager::DNS
|
2
|
+
class Manager
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
def self.method_missing(meth, *args, &block)
|
6
|
+
if client.respond_to?(meth)
|
7
|
+
client.send(meth, *args, &block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.client
|
12
|
+
instance.tap {|i| i.authorize }
|
13
|
+
end
|
14
|
+
|
15
|
+
def setup(options={})
|
16
|
+
unless Skypager.config.dnsimple_api_token
|
17
|
+
if dnsimple_api_token = options.fetch(:dnsimple_api_token) { ask("What is the DNSimple API Token?", String) }
|
18
|
+
Skypager.config.set 'dnsimple_api_token', dnsimple_api_token
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
unless Skypager.config.dnsimple_username
|
23
|
+
if dnsimple_username = options.fetch(:dnsimple_username) { ask("What is the DNSimple Username or email?", String) }
|
24
|
+
Skypager.config.set 'dnsimple_username', dnsimple_username
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def authorize
|
30
|
+
@authorized ||= begin
|
31
|
+
DNSimple::Client.api_token = Skypager.config.dnsimple_api_token
|
32
|
+
DNSimple::Client.username = Skypager.config.dnsimple_username
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def parent_domain
|
37
|
+
@parent_domain ||= DNSimple::Domain.find(Skypager.config.domain)
|
38
|
+
end
|
39
|
+
|
40
|
+
def domain_records
|
41
|
+
DNSimple::Record.all(parent_domain)
|
42
|
+
end
|
43
|
+
|
44
|
+
def cname_records
|
45
|
+
domain_records.select do |record|
|
46
|
+
record.record_type == "CNAME"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def deployment_alias_records
|
51
|
+
cname_records.select do |record|
|
52
|
+
record.content.include? "s3-website-us-east-1.amazonaws.com"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Used to link up a skypager internal site to an S3 Bucket
|
57
|
+
def setup_cname internal_name, external_host
|
58
|
+
authorize
|
59
|
+
|
60
|
+
unless cname_records.find {|r| r.name == internal_name }
|
61
|
+
DNSimple::Record.create(parent_domain, internal_name, 'CNAME', external_host)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'middleman-core' unless defined?(::Middleman)
|
2
|
+
|
3
|
+
module Skypager
|
4
|
+
class Extension < ::Middleman::Extension
|
5
|
+
|
6
|
+
option :site_name, nil, 'The name of this site'
|
7
|
+
option :synced_folders, {}, 'The settings for dropbox source syncing'
|
8
|
+
option :data_sources, [], 'The data source mappings for this site'
|
9
|
+
option :deploy_options, {}, 'Deploy options: domain, bucket_name, use_cdn, aliases, auto_deploy'
|
10
|
+
|
11
|
+
def initialize(app, options_hash={}, &block)
|
12
|
+
# if only skypager/extension is required this will be necessary
|
13
|
+
require 'skypager' unless defined?(Skypager::Data)
|
14
|
+
|
15
|
+
super
|
16
|
+
|
17
|
+
app.include(ClassMethods)
|
18
|
+
|
19
|
+
options_hash.reverse_merge!(:data_sources => {},
|
20
|
+
:synced_folders => {},
|
21
|
+
:deploy_options => {},
|
22
|
+
:site_name => File.basename(app.root))
|
23
|
+
|
24
|
+
options_hash.each do |key, value|
|
25
|
+
app.set(key, value)
|
26
|
+
end
|
27
|
+
|
28
|
+
unless $skypager_command
|
29
|
+
app.ready do
|
30
|
+
synced_folders.each do |name, folder|
|
31
|
+
folder.sync()
|
32
|
+
end
|
33
|
+
|
34
|
+
# Load the data source and save it to disk
|
35
|
+
data_sources.each do |name, source|
|
36
|
+
source.refresh
|
37
|
+
source.save_to_disk unless source.persisted?
|
38
|
+
data.callbacks(name, -> { source.data })
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# In development, we will try to refresh remote data stores (e.g. google spreadsheets)
|
43
|
+
# before every request once every minute or so
|
44
|
+
app.before do
|
45
|
+
data_sources.values.each do |data_source|
|
46
|
+
data_source.refresh_if_stale?
|
47
|
+
end if development?
|
48
|
+
end
|
49
|
+
|
50
|
+
app.after_build do
|
51
|
+
if deploy_options[:auto_deploy]
|
52
|
+
app.site.deploy()
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
helpers do
|
60
|
+
def dropbox(path)
|
61
|
+
end
|
62
|
+
|
63
|
+
def site
|
64
|
+
@site ||= Skypager::Site.new(site_name)
|
65
|
+
end
|
66
|
+
|
67
|
+
def deployment
|
68
|
+
@deployment ||= Skypager::Sync::Amazon.client(deploy_options)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module ClassMethods
|
73
|
+
def build_path
|
74
|
+
Pathname(build_dir)
|
75
|
+
end
|
76
|
+
|
77
|
+
def data_path
|
78
|
+
Pathname(data_dir)
|
79
|
+
end
|
80
|
+
|
81
|
+
def source_path
|
82
|
+
Pathname(source_dir)
|
83
|
+
end
|
84
|
+
|
85
|
+
def deploy_to(provider, options={})
|
86
|
+
self.deploy_options ||= {}
|
87
|
+
|
88
|
+
if provider == :amazon || provider == :aws
|
89
|
+
self.deploy_options[:domain] = options[:domain] || Skypager.config.domain
|
90
|
+
self.deploy_options[:bucket_name] = options[:bucket_name] || "#{ site_name }.#{ deploy_options[:domain] }"
|
91
|
+
self.deploy_options[:custom_domain] = options[:custom_domain]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def dropbox_sync(*args)
|
96
|
+
folder = args.first
|
97
|
+
options = args.extract_options!
|
98
|
+
|
99
|
+
local_path = folder
|
100
|
+
remote_path ||= options[:to] || options[:with] || args[1] || local_path
|
101
|
+
|
102
|
+
dropbox_data[:folders] ||= {}
|
103
|
+
|
104
|
+
f = dropbox_data[:folders][folder] ||= {
|
105
|
+
remote_path: remote_path,
|
106
|
+
local_path: local_path,
|
107
|
+
type: "dropbox",
|
108
|
+
cursor: nil,
|
109
|
+
site_name: site_name,
|
110
|
+
folder: folder
|
111
|
+
}
|
112
|
+
|
113
|
+
dropbox_data[:folders][folder].merge!(options)
|
114
|
+
|
115
|
+
self.synced_folders[folder] = Skypager::Sync::Folder.new(f.merge(app: self, root:Pathname(root)))
|
116
|
+
|
117
|
+
save_dropbox_settings
|
118
|
+
end
|
119
|
+
|
120
|
+
def syncables
|
121
|
+
@syncables ||= Array(synced_folders.values)
|
122
|
+
end
|
123
|
+
|
124
|
+
def syncable
|
125
|
+
syncables.first
|
126
|
+
end
|
127
|
+
|
128
|
+
def save_dropbox_settings
|
129
|
+
json = dropbox_data.to_json
|
130
|
+
dropbox_data_file.open('w+') {|fh| fh.write(json) }
|
131
|
+
end
|
132
|
+
|
133
|
+
def dropbox_data
|
134
|
+
unless Pathname(data_dir).exist?
|
135
|
+
FileUtils.mkdir_p Pathname(data_dir)
|
136
|
+
end
|
137
|
+
|
138
|
+
@dropbox_data ||= begin
|
139
|
+
Hashie::Mash.new(JSON.parse(dropbox_data_file.read))
|
140
|
+
rescue
|
141
|
+
Hashie::Mash.new({})
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def dropbox_data_file
|
146
|
+
Pathname(data_dir).join('dropbox.json').tap do |path|
|
147
|
+
unless path.exist?
|
148
|
+
path.open('w+') {|fh| fh.write("{}") }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def google_spreadsheets
|
154
|
+
Skypager::Data::GoogleSpreadsheet
|
155
|
+
end
|
156
|
+
|
157
|
+
def map_data_source(name, *args)
|
158
|
+
options = args.extract_options!
|
159
|
+
options[:data_source] = name
|
160
|
+
scope = options[:scope] = args.first
|
161
|
+
|
162
|
+
_data_source = data_sources[name]
|
163
|
+
|
164
|
+
if !_data_source
|
165
|
+
raise "Could not find a data source named #{ name }"
|
166
|
+
end
|
167
|
+
|
168
|
+
if scope && !_data_source.has_scope?(scope)
|
169
|
+
raise "Invalid scope for #{name} data source"
|
170
|
+
end
|
171
|
+
|
172
|
+
Skypager::Data::SourceRoutesProxy.new(self, _data_source, options)
|
173
|
+
end
|
174
|
+
|
175
|
+
def data_source(name, options=nil, &block)
|
176
|
+
if options.nil? && !block_given? && self.data_sources[name.to_sym]
|
177
|
+
return self.data_sources[name.to_sym]
|
178
|
+
end
|
179
|
+
|
180
|
+
source = case
|
181
|
+
when options[:type].to_s == "google"
|
182
|
+
unless existing = google_spreadsheets[options[:key] || options[:title]]
|
183
|
+
raise 'Could not find a spreadsheet. You can use the `skypager list spreadsheets` command to find the right one, or the `skypager create data source` command to create a new one.'
|
184
|
+
end
|
185
|
+
|
186
|
+
existing
|
187
|
+
when options[:type].to_s == "excel"
|
188
|
+
|
189
|
+
when options[:type].to_s == "request"
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
(self.data_sources[name.to_sym] = source).tap do |ds|
|
194
|
+
ds.instance_eval(&block) if block.respond_to?(:call)
|
195
|
+
ds.slug_column = (options[:slug] || options[:slug_column]) if (options[:slug] || options[:slug_column])
|
196
|
+
ds.path ||= Pathname(root).join(data_dir)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
::Middleman::Extensions.register(:skypager, Skypager::Extension)
|