skypager 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|