strobe 0.1.6 → 0.2.0.beta.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.
@@ -8,18 +8,25 @@ require 'active_support/core_ext/hash/indifferent_access'
8
8
  require 'active_support/core_ext/string/inflections'
9
9
 
10
10
  module Strobe
11
- autoload :Association, 'strobe/association'
12
- autoload :CLI, 'strobe/cli'
13
- autoload :Collection, 'strobe/collection'
14
- autoload :Connection, 'strobe/connection'
15
- autoload :IdentityMap, 'strobe/identity_map'
16
- autoload :Key, 'strobe/key'
17
- autoload :Resources, 'strobe/resources'
18
- autoload :Validations, 'strobe/validations'
11
+ autoload :Association, 'strobe/association'
12
+ autoload :CLI, 'strobe/cli'
13
+ autoload :Collection, 'strobe/collection'
14
+ autoload :Config, 'strobe/config'
15
+ autoload :Connection, 'strobe/connection'
16
+ autoload :IdentityMap, 'strobe/identity_map'
17
+ autoload :Key, 'strobe/key'
18
+ autoload :Resources, 'strobe/resources'
19
+ autoload :Validations, 'strobe/validations'
20
+ autoload :ExceptionNotifier, 'strobe/exception_notifier'
21
+
22
+ module Addons
23
+ # Stubb out the Addons module
24
+ end
19
25
 
20
26
  module Middleware
21
- autoload :Proxy, 'strobe/middleware/proxy'
22
- autoload :Rewrite, 'strobe/middleware/rewrite'
27
+ autoload :Addons, 'strobe/middleware/addons'
28
+ autoload :Proxy, 'strobe/middleware/proxy'
29
+ autoload :Rewrite, 'strobe/middleware/rewrite'
23
30
  end
24
31
 
25
32
  module Resource
@@ -30,12 +37,21 @@ module Strobe
30
37
 
31
38
 
32
39
  # Errors
33
- class StrobeError < StandardError ; end
34
- class RequestError < StrobeError ; end
35
- class ResourceNotFoundError < StrobeError ; end
36
- class ServerError < StrobeError ; end
37
- class UnauthenticatedError < StrobeError ; end
38
- class ValidationError < StrobeError ; end
40
+ class StrobeError < StandardError ; end
41
+ class ResourceNotFoundError < StrobeError ; end
42
+ class UnauthenticatedError < StrobeError ; end
43
+ class ValidationError < StrobeError ; end
44
+
45
+ class NotifiableError < StrobeError
46
+ attr_reader :data
47
+ def initialize(message = nil, data = nil)
48
+ super(message)
49
+ @data = data
50
+ end
51
+ end
52
+
53
+ class RequestError < NotifiableError ; end
54
+ class ServerError < NotifiableError ; end
39
55
 
40
56
  def self.connection
41
57
  @connection
@@ -0,0 +1,130 @@
1
+ require 'oauth'
2
+
3
+ module Strobe
4
+ module Addons
5
+ class Social
6
+ TWITTER_URL = "http://api.twitter.com"
7
+ REQUEST_TOKEN_SECRETS = {}
8
+ ACCESS_TOKEN_SECRETS = {}
9
+
10
+ def initialize(settings)
11
+ @settings = settings
12
+ @cookie_key = "twitter_access_token"
13
+ @consumer_key = settings['social.twitter.consumer_key']
14
+ @consumer_secret = settings['social.twitter.consumer_secret']
15
+
16
+ unless @consumer_key
17
+ raise "Social addon requires `social.twitter.consumer_key` configuration"
18
+ end
19
+
20
+ unless @consumer_secret
21
+ raise "Social addon requires `social.twitter.consumer_secret` configuration"
22
+ end
23
+
24
+ @consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret, :site => TWITTER_URL)
25
+ end
26
+
27
+ def call(env)
28
+ dup._call(env)
29
+ end
30
+
31
+ def _call(env)
32
+ @request = Rack::Request.new(env)
33
+
34
+ case env["PATH_INFO"]
35
+ when "/twitter/authenticate"
36
+ get_request_token
37
+ when "/twitter/callback"
38
+ get_access_token
39
+ when %r[^/twitter(.*)$]
40
+ proxy
41
+ else
42
+ msg = "Not found"
43
+ [404, {"Content-Length" => msg.bytesize.to_s}, [msg]]
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def get_request_token
50
+ request_token = @consumer.get_request_token(:oauth_callback => callback_url)
51
+ REQUEST_TOKEN_SECRETS[request_token.token] = request_token.secret
52
+
53
+ response = Rack::Response.new
54
+ response.redirect(request_token.authorize_url(:oauth_callback => callback_url))
55
+ response.finish
56
+ end
57
+
58
+ def get_access_token
59
+ response = Rack::Response.new
60
+
61
+ oauth_token = @request.params["oauth_token"]
62
+ oauth_verifier = @request.params["oauth_verifier"]
63
+ request_token = REQUEST_TOKEN_SECRETS[oauth_token]
64
+
65
+ if oauth_token && request_token
66
+ request_token = OAuth::RequestToken.new(@consumer, oauth_token, request_token)
67
+ access_token = request_token.get_access_token(:oauth_verifier => oauth_verifier)
68
+ ACCESS_TOKEN_SECRETS[access_token.token] = access_token.secret
69
+
70
+ response.body = ['<html><head><script>window.opener?window.close():(window.location = "/")</script></head></html>']
71
+ response.set_cookie(@cookie_key, :value => access_token.token, :path => "/")
72
+ response.finish
73
+ else
74
+ msg = "Could not match oauth response with an oauth request. Was the server restarted?\n"
75
+ [404, {"Content-Type" => "text/plain", "Content-Length" => msg.bytesize.to_s}, msg]
76
+ end
77
+ end
78
+
79
+ def proxy
80
+ token = @request.cookies[@cookie_key]
81
+ access_token = OAuth::AccessToken.new(@consumer, token, ACCESS_TOKEN_SECRETS[token])
82
+
83
+ request_method = @request.request_method.to_s.upcase
84
+ request_path = @request.fullpath.sub(%r<^/_strobe/social/twitter>, "")
85
+ request_body = @request.body.read
86
+ request_headers = {}
87
+ request_headers["Content-Type"] = @request.content_type if @request.content_type.present?
88
+ request_headers["Content-Length"] = @request.content_length if @request.content_length.present?
89
+
90
+ http = Net::HTTP.new(URI.parse(TWITTER_URL).host, URI.parse(TWITTER_URL).port)
91
+ oauth_request = Net::HTTPGenericRequest.new(request_method, request_body.present?, request_method != "HEAD", request_path, request_headers)
92
+ oauth_request.body = request_body if request_body.present?
93
+ oauth_request.oauth!(http, @consumer, access_token)
94
+
95
+ response_headers = {}
96
+
97
+ response_headers["Authorization"] = oauth_request["Authorization"]
98
+ response_headers["X-Strobe-Forward-Headers"] = "Authorization"
99
+
100
+ response_headers["X-Strobe-Location"] = URI.join(TWITTER_URL, request_path).to_s
101
+ response_headers["X-Strobe-Redirect-Method"] = request_method
102
+
103
+ if oauth_request["Content-Type"].present?
104
+ response_headers["X-Strobe-Forward-Headers"] += ",Content-Type"
105
+ response_headers["Content-Type"] = oauth_request["Content-Type"]
106
+ end
107
+
108
+ if oauth_request["Content-Length"].present?
109
+ response_headers["X-Strobe-Forward-Headers"] += ",Content-Length"
110
+ response_headers["Content-Length"] = oauth_request["Content-Length"]
111
+ end
112
+
113
+ [302, response_headers, [request_body]]
114
+ end
115
+
116
+ def root_url
117
+ url_for("")
118
+ end
119
+
120
+ def callback_url
121
+ url_for("/twitter/callback")
122
+ end
123
+
124
+ def url_for(path)
125
+ # host_with_port returns http://localhost:9293 in tests
126
+ "#{@request.scheme}://#{@request.host_with_port.sub(%r<^https?://>, "")}#{@request.script_name}#{path}"
127
+ end
128
+ end
129
+ end
130
+ end
@@ -24,9 +24,15 @@ module Strobe
24
24
  IdentityMap.wrap do
25
25
  super
26
26
  end
27
- rescue Strobe::StrobeError => e
27
+ rescue NotifiableError => e
28
28
  raise if $ARGV.include?('--backtrace')
29
- abort "[ERROR] #{e.message}"
29
+ STDERR.puts "[ERROR] #{e.message}"
30
+ ExceptionNotifier.notify(e, :action => args.join(" "))
31
+ abort
32
+ rescue StrobeError => e
33
+ raise if $ARGV.include?('--backtrace')
34
+ STDERR.puts "[ERROR] #{e.message}"
35
+ abort
30
36
  rescue Interrupt
31
37
  abort "\nQuitting..."
32
38
  end
@@ -34,7 +40,12 @@ module Strobe
34
40
  def self.action(name, *args, &blk)
35
41
  @haxliases ||= []
36
42
  @haxliases << name.to_s
37
- usage = self == Users ? "users #{name}" : name
43
+ options = args.last if args.last.is_a?(Hash)
44
+ usage = if options && options[:usage]
45
+ options[:usage]
46
+ else
47
+ self == Users ? "users #{name}" : name
48
+ end
38
49
  desc usage, *args
39
50
  define_method("__hax__#{name}", &blk)
40
51
  map name => "__hax__#{name}"
@@ -75,12 +86,13 @@ module Strobe
75
86
  options[:path] || Dir.pwd
76
87
  end
77
88
 
78
- def config(app = nil)
79
- if app
80
- @config = settings.configs.find { |c| c[:application_id] = app[:id] }
81
- end
89
+ def config
90
+ return @config if @config
91
+ @config = Config.new_or_stub(path) || Config.stub!(path)
92
+ end
82
93
 
83
- @config ||= Settings.application(path)
94
+ def settings
95
+ @settings ||= Settings.for(path)
84
96
  end
85
97
 
86
98
  def invoke(action, *args)
@@ -93,10 +105,6 @@ module Strobe
93
105
  @resource
94
106
  end
95
107
 
96
- def settings
97
- @settings ||= Settings.new
98
- end
99
-
100
108
  def until_success(&blk)
101
109
  until @success
102
110
  retval = blk.call
@@ -10,6 +10,30 @@ module Strobe
10
10
  end
11
11
  end
12
12
 
13
+ method_option "global", :type => :boolean, :banner => "Set option globally (for all applications)"
14
+ method_option "unset", :type => :boolean, :banner => "Unset a config option"
15
+ action "config", "configure strobe", :usage => "config [KEY] [VALUE]" do |key, val = nil|
16
+ case
17
+ when val
18
+ if options[:global]
19
+ settings.global_set! key, val
20
+ say "Globally setting `#{key}` to `#{val}`"
21
+ else
22
+ settings.set! key, val
23
+ say "Setting `#{key}` to `#{val}`"
24
+ end
25
+ when options[:unset]
26
+ settings.unset!(key)
27
+ say "Unsetting `#{key}`"
28
+ else
29
+ if val = settings[key]
30
+ say "#{key}: #{val}"
31
+ else
32
+ say "No value for `#{key}`"
33
+ end
34
+ end
35
+ end
36
+
13
37
  action "users", 'manage users' do |*args|
14
38
  argv = $ARGV[1..-1]
15
39
  argv = [ 'list' ] + argv if args.empty?
@@ -23,6 +47,20 @@ module Strobe
23
47
  Launchy.open("http://#{url}")
24
48
  end
25
49
 
50
+ action "deploys", "manage deploys" do
51
+ id = options['application-id'] || config[:application_id]
52
+ app = Application.new(:id => id)
53
+ deploys = app.deploys.all
54
+
55
+ table deploys do |t|
56
+ t.column :key do |d| d[:id][0..7] end
57
+ t.column :parent do |d| d[:parent] ? d[:parent][0..7] : "" end
58
+ t.column :name
59
+ t.column :email
60
+ t.column :message
61
+ end
62
+ end
63
+
26
64
  action "signup", "signup for a new Strobe account" do
27
65
  resource Signup.new :account => { :name => 'default' }
28
66
 
@@ -46,7 +84,7 @@ module Strobe
46
84
  end
47
85
 
48
86
  save do
49
- settings[:token] = resource['user.authentication_token']
87
+ settings.global_set! :token, resource['user.authentication_token']
50
88
  settings.authenticate!
51
89
  say "Your account was created successfully."
52
90
  end
@@ -63,7 +101,7 @@ module Strobe
63
101
  password :password, :into => "user.password"
64
102
 
65
103
  save do
66
- settings[:token] = resource['user.authentication_token']
104
+ settings.global_set! :token, resource['user.authentication_token']
67
105
  settings.authenticate!
68
106
  say "The computer has been registered with the account."
69
107
  end
@@ -109,8 +147,11 @@ module Strobe
109
147
  ask :application_name, :into => "name"
110
148
 
111
149
  save do
112
- config[:application_id] ||= resource[:id]
113
- settings.register_app_path(config.filename)
150
+ unless config[:application_id]
151
+ config.set_application_id! resource[:id]
152
+ end
153
+
154
+ settings.register_app_path(path)
114
155
  end
115
156
  end
116
157
 
@@ -159,7 +200,10 @@ module Strobe
159
200
  end
160
201
 
161
202
  delete do
162
- config(resource).delete
203
+ if config = settings.configs.find { |c| c[:application_id] == id.to_i }
204
+ config.unset_application_id!
205
+ settings.unregister_app_path config.application_path
206
+ end
163
207
  say "The application has been deleted"
164
208
  end
165
209
  end
@@ -169,16 +213,14 @@ module Strobe
169
213
  action "register", "registers a local application with an existing Strobe application" do
170
214
  if config[:application_id] && app = Application.get(config[:application_id])
171
215
  say "The directory is already registered with the application `#{app[:name]}`"
172
- unless agree "Continue? [Yn] "
173
- say "Aborting..."
174
- end
175
- config.delete
216
+ say "Aborting..."
217
+ exit 1
176
218
  end
177
219
 
178
220
  application = options['application-id'] ? Application.get!(options['application-id']) : pick_application
179
221
 
180
- config[:application_id] = application[:id]
181
- settings.register_app_path(config.filename)
222
+ config.set_application_id! application[:id]
223
+ settings.register_app_path(path)
182
224
 
183
225
  say "You can now deploy to #{application[:name]} (http://#{application[:url]})"
184
226
  end
@@ -18,8 +18,10 @@ module Strobe
18
18
 
19
19
  def preview_html_application
20
20
  app = Rack::Static.new lambda { |e| [] }, :urls => [ '/' ], :root => '.'
21
- app = Middleware::Rewrite.new(Middleware::Proxy.new(app))
22
- Server.start :app => app, :host => '0.0.0.0', :Port => 9292
21
+ app = Middleware::Rewrite.new(app)
22
+ app = Middleware::Addons.new(app, :settings => settings, :config => config)
23
+ app = Middleware::Proxy.new(app)
24
+ Server.start :app => app, :host => '0.0.0.0', :server => "thin", :Port => 9292
23
25
  end
24
26
 
25
27
  def preview_sproutcore_application
@@ -12,34 +12,71 @@ module Strobe
12
12
  Pathname.new(root).join(".strobe/#{file}")
13
13
  end
14
14
 
15
- def self.application(root)
16
- new application_config_file(root)
15
+ def self.for(path)
16
+ new(application_config_file(path), global_config_file)
17
+ end
18
+
19
+ def self.global
20
+ new(nil, global_config_file)
17
21
  end
18
22
 
19
23
  attr_reader :filename
20
24
 
21
- def initialize(filename = self.class.global_config_file)
22
- @filename = Pathname.new(filename)
25
+ def initialize(local_filename, global_filename)
26
+ @local_filename = Pathname.new(local_filename) if local_filename
27
+ @global_filename = Pathname.new(global_filename)
23
28
 
24
- if @filename.exist?
25
- @hash = YAML.load_file(@filename)
29
+ if @local_filename && @local_filename.exist?
30
+ @local_hash = YAML.load_file(@local_filename)
26
31
  end
27
32
 
28
- @hash = {} unless Hash === @hash
29
- end
33
+ @local_hash = {} unless Hash === @local_hash
34
+
35
+ if @global_filename.exist?
36
+ @global_hash = YAML.load_file(@global_filename)
37
+ end
30
38
 
31
- def application_path
32
- filename.sub %r[/.strobe/config(\.[a-z]+)?$]i, ''
39
+ @global_hash = {} unless Hash === @global_hash
33
40
  end
34
41
 
42
+ # def application_path
43
+ # filename.sub %r[/.strobe/config(\.[a-z]+)?$]i, ''
44
+ # end
45
+
35
46
  def [](k)
36
- ENV[key(k)] || @hash[key(k)]
47
+ k = key(k)
48
+ ENV[k] || @local_hash[k] || @global_hash[k]
49
+ end
50
+
51
+ def fetch(k, v)
52
+ self[k] || v
53
+ end
54
+
55
+ def set!(k, v)
56
+ @local_hash[key(k)] = v
57
+ persist! @local_hash, @local_filename
58
+ v
37
59
  end
38
60
 
39
- def []=(k, val)
40
- @hash[key(k)] = val
41
- persist!
42
- val
61
+ def global_set!(k, v)
62
+ @global_hash[key(k)] = v
63
+ persist! @global_hash, @global_filename
64
+ v
65
+ end
66
+
67
+ def unset!(k)
68
+ k = key(k)
69
+ if @local_hash[k]
70
+ @local_hash.delete(k)
71
+ persist! @local_hash, @local_filename
72
+ end
73
+
74
+ if @global_hash[k]
75
+ @global_hash.delete(k)
76
+ persist! @global_hash, @global_filename
77
+ end
78
+
79
+ nil
43
80
  end
44
81
 
45
82
  def connection
@@ -59,38 +96,47 @@ module Strobe
59
96
  end
60
97
  end
61
98
 
62
- def delete
63
- FileUtils.rm_f(@filename)
64
- end
65
-
66
99
  def configs
67
100
  @configs ||= begin
68
- self[:applications] ||= []
69
- self[:applications].map do |path|
70
- next unless File.file?(path)
71
- self.class.new(path)
72
- end.compact
101
+ application_paths.map do |path|
102
+ Config.new_or_stub(path)
103
+ end
73
104
  end
74
105
  end
75
106
 
76
- def register_app_path(path)
77
- self[:applications] ||= []
78
- self[:applications] |= [File.expand_path(path.to_s)]
79
- self[:applications] = self[:applications].select do |path|
80
- File.file?(path)
107
+ def application_paths
108
+ paths = self[:applications] || []
109
+
110
+ # Handle legacy application paths
111
+ # TODO: When it's safe, remove this code
112
+ paths.map! do |p|
113
+ if File.file?(p) && p =~ /\.strobe/
114
+ File.expand_path('../..', p)
115
+ else
116
+ p
117
+ end
118
+ end
119
+
120
+ # TODO: Handle the logacy case where applications points
121
+ # to a .strobe/config file
122
+ paths.delete_if do |path|
123
+ !(File.file?("#{path}/.strobe/config") ||
124
+ File.file?("#{path}/strobe/config.json"))
81
125
  end
82
126
 
83
- persist!
127
+ paths
84
128
  end
85
129
 
86
- def unregister_app_path(path)
87
- self[:applications] ||= []
88
- self[:applications].delete File.expand_path(path.to_s)
89
- self[:applications] = self[:applications].select do |path|
90
- File.file?(path)
91
- end
130
+ def register_app_path(path)
131
+ paths = application_paths
132
+ paths |= [File.expand_path(path.to_s)]
133
+ global_set! :applications, paths
134
+ end
92
135
 
93
- persist!
136
+ def unregister_app_path(path)
137
+ paths = application_paths
138
+ paths.delete File.expand_path(path.to_s)
139
+ global_set! :applications, paths
94
140
  end
95
141
 
96
142
  private
@@ -99,11 +145,18 @@ module Strobe
99
145
  "STROBE_#{key}".upcase
100
146
  end
101
147
 
102
- def persist!
103
- # Persist the setting
104
- FileUtils.mkdir_p(@filename.dirname)
105
- File.open(@filename, "wb") do |file|
106
- file.puts @hash.to_yaml
148
+ def persist!(hash, filename)
149
+ FileUtils.mkdir_p(filename.dirname)
150
+ File.open(filename, "wb") do |file|
151
+ file.puts hash.to_yaml
152
+ end
153
+ end
154
+
155
+ def check_app_paths
156
+ self[:applications] ||= []
157
+ self[:applications] = self[:applications].select do |path|
158
+ File.file?("#{path}/.strobe") ||
159
+ File.file?("#{path}/strobe/config.json")
107
160
  end
108
161
  end
109
162
  end