skypager 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/Gemfile +9 -0
  4. data/LICENSE.txt +22 -0
  5. data/Rakefile +21 -0
  6. data/bin/skypager +17 -0
  7. data/examples/.gitignore +4 -0
  8. data/examples/blog-site/.gitignore +18 -0
  9. data/examples/blog-site/.pryrc +4 -0
  10. data/examples/blog-site/Gemfile +8 -0
  11. data/examples/blog-site/config.rb +17 -0
  12. data/examples/blog-site/data/dropbox.json +1 -0
  13. data/examples/blog-site/source/images/background.png +0 -0
  14. data/examples/blog-site/source/images/middleman.png +0 -0
  15. data/examples/blog-site/source/index.html.erb +10 -0
  16. data/examples/blog-site/source/javascripts/all.js +1 -0
  17. data/examples/blog-site/source/layouts/layout.erb +19 -0
  18. data/examples/blog-site/source/posts/introduction-to-skypager.html.md +23 -0
  19. data/examples/blog-site/source/posts/skypager-and-dnsimple-and-amazon-web-services-combo.html.md +9 -0
  20. data/examples/blog-site/source/stylesheets/all.css +55 -0
  21. data/examples/blog-site/source/stylesheets/normalize.css +375 -0
  22. data/examples/gallery-site/.gitignore +18 -0
  23. data/examples/gallery-site/.pryrc +4 -0
  24. data/examples/gallery-site/Gemfile +11 -0
  25. data/examples/gallery-site/config.rb +38 -0
  26. data/examples/gallery-site/data/dropbox.json +1 -0
  27. data/examples/gallery-site/data/galleries.json +1 -0
  28. data/examples/gallery-site/source/gallery.html.erb +7 -0
  29. data/examples/gallery-site/source/images/background.png +0 -0
  30. data/examples/gallery-site/source/images/galleries/cristian-gallery-1/001.jpg +0 -0
  31. data/examples/gallery-site/source/images/galleries/cristian-gallery-1/002.jpg +0 -0
  32. data/examples/gallery-site/source/images/galleries/cristian-gallery-1/003.jpg +0 -0
  33. data/examples/gallery-site/source/images/galleries/cristian-gallery-1/004.jpg +0 -0
  34. data/examples/gallery-site/source/images/galleries/luca-gallery-1/001.jpg +0 -0
  35. data/examples/gallery-site/source/images/galleries/luca-gallery-1/002.JPG +0 -0
  36. data/examples/gallery-site/source/images/galleries/luca-gallery-1/003.jpg +0 -0
  37. data/examples/gallery-site/source/images/galleries/luca-gallery-1/004.JPG +0 -0
  38. data/examples/gallery-site/source/images/middleman.png +0 -0
  39. data/examples/gallery-site/source/index.html.erb +10 -0
  40. data/examples/gallery-site/source/javascripts/all.js +1 -0
  41. data/examples/gallery-site/source/layouts/layout.erb +20 -0
  42. data/examples/gallery-site/source/stylesheets/all.css +0 -0
  43. data/examples/gallery-site/source/stylesheets/normalize.css +375 -0
  44. data/examples/gallery-site/source/tutorial.md +151 -0
  45. data/lib/skypager.rb +92 -0
  46. data/lib/skypager/build_server.rb +17 -0
  47. data/lib/skypager/cli/commands/config.rb +58 -0
  48. data/lib/skypager/cli/commands/create.rb +98 -0
  49. data/lib/skypager/cli/commands/deploy.rb +30 -0
  50. data/lib/skypager/cli/commands/edit.rb +32 -0
  51. data/lib/skypager/cli/commands/list.rb +12 -0
  52. data/lib/skypager/cli/commands/setup.rb +124 -0
  53. data/lib/skypager/cli/commands/sync.rb +18 -0
  54. data/lib/skypager/configuration.rb +173 -0
  55. data/lib/skypager/data.rb +8 -0
  56. data/lib/skypager/data/excel_spreadsheet.rb +8 -0
  57. data/lib/skypager/data/google_spreadsheet.rb +225 -0
  58. data/lib/skypager/data/request.rb +12 -0
  59. data/lib/skypager/data/source.rb +171 -0
  60. data/lib/skypager/data/source_routes_proxy.rb +30 -0
  61. data/lib/skypager/dns.rb +65 -0
  62. data/lib/skypager/extension.rb +203 -0
  63. data/lib/skypager/middleman/commands/data.rb +0 -0
  64. data/lib/skypager/middleman/commands/deploy.rb +0 -0
  65. data/lib/skypager/middleman/commands/sync.rb +0 -0
  66. data/lib/skypager/site.rb +208 -0
  67. data/lib/skypager/sync.rb +23 -0
  68. data/lib/skypager/sync/amazon.rb +171 -0
  69. data/lib/skypager/sync/dropbox.rb +173 -0
  70. data/lib/skypager/sync/dropbox/delta.rb +67 -0
  71. data/lib/skypager/sync/folder.rb +235 -0
  72. data/lib/skypager/sync/google.rb +143 -0
  73. data/lib/skypager/tar.rb +77 -0
  74. data/lib/skypager/version.rb +3 -0
  75. data/skypager.gemspec +40 -0
  76. data/spec/lib/skypager/configuration_spec.rb +5 -0
  77. data/spec/lib/skypager/data_spec.rb +5 -0
  78. data/spec/lib/skypager/site_spec.rb +5 -0
  79. data/spec/spec_helper.rb +14 -0
  80. data/spec/support/json_helper.rb +7 -0
  81. metadata +383 -0
@@ -0,0 +1,12 @@
1
+ module Skypager
2
+ module Data
3
+ class Request < Source
4
+ requires :host,
5
+ :path
6
+
7
+ def initialize name, options={}
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
@@ -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
@@ -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)