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.
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)