fjords-client 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ Gemfile.lock
2
+ pkg
3
+ .DS_Store
4
+ .ruby-version
5
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fjords.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem "rake"
8
+ gem "rspec"
9
+ gem "webmock"
10
+ end
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+ desc "Run RSpec"
6
+ RSpec::Core::RakeTask.new do |spec|
7
+ spec.pattern = 'spec/**/*_spec.rb'
8
+ spec.rspec_opts = ['--color', '--format nested']
9
+ end
10
+
11
+ desc "Run tests"
12
+ task :test => [:spec]
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/fjords-client/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Thomas Reynolds"]
6
+ gem.email = ["me@tdreyno.com"]
7
+ gem.description = %q{Fjords.cc API}
8
+ gem.summary = %q{Fjords.cc API}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "fjords-client"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Fjords::Client::VERSION
17
+
18
+ gem.add_dependency("rest-client", "1.6.7")
19
+ gem.add_dependency("addressable", "2.3.2")
20
+ gem.add_dependency("stripe")
21
+ end
@@ -0,0 +1,391 @@
1
+ require 'json'
2
+ require 'rest_client'
3
+ require 'addressable/uri'
4
+ require 'tmpdir'
5
+ require 'digest/md5'
6
+ require 'pathname'
7
+
8
+ require "stripe"
9
+ Stripe.api_key = 'pk_vfcVMdOHZGAFgqLGSEtlSKzsXeBYI'
10
+
11
+ module Fjords
12
+ class Token
13
+ attr_reader :token
14
+
15
+ def initialize(token)
16
+ @token = token
17
+ end
18
+ end
19
+
20
+ class Client
21
+
22
+ EXCLUSION_GLOBS = ['..', '.', '*~', '#*#', '.*', '*/.*']
23
+
24
+ class << self
25
+ attr_accessor :host
26
+
27
+ def shared
28
+ @_shared_client ||= new
29
+ end
30
+
31
+ def host
32
+ @_host ||= "https://api.fjords.cc/v1"
33
+ end
34
+
35
+ def host=(host)
36
+ @_host = host
37
+ end
38
+ end
39
+
40
+ # Delegate methods
41
+ [
42
+ :login,
43
+ :logged_in?,
44
+ :push,
45
+ :prepare_push,
46
+ :sites,
47
+ :delete_site,
48
+ :signup,
49
+ :username_available?,
50
+ :get_stripe_token,
51
+ :logout,
52
+ :validate_domain,
53
+ :bugreport,
54
+ :info
55
+ ].each do |method|
56
+ module_eval(%Q{
57
+ def self.#{method}(*args, &block) # def self.login(*args, &block)
58
+ shared.__send__(:#{method}, *args, &block) # shared.__send__(:login, *args, &block)
59
+ end # end
60
+ })
61
+ end
62
+
63
+ class ClientError < RuntimeError
64
+ def initialize(e=nil)
65
+ super()
66
+
67
+ @e = e
68
+ end
69
+
70
+ def report_data
71
+ @e.backtrace
72
+ end
73
+ end
74
+ class Forbidden < ClientError; end
75
+ class ConnectionRefused < ClientError; end
76
+ class ResourceNotFound < ClientError; end
77
+ class LoginInvalid < ClientError; end
78
+ class NotAuthorizedError < ClientError; end
79
+ class InternalServerError < ClientError; end
80
+ class BadPathError < ClientError; end
81
+ class Conflict < ClientError; end
82
+ class UploadTooLarge < ClientError; end
83
+ class NothingToUpload < ClientError; end
84
+
85
+ attr_reader :username
86
+
87
+ def logged_in?
88
+ read_token
89
+ end
90
+
91
+ def signup(email, username, password, stripe_token)
92
+ json = post("/accounts", {
93
+ :email => email,
94
+ :username => username,
95
+ :password => password,
96
+ :stripe_token => stripe_token
97
+ })
98
+
99
+ token = json['token']
100
+
101
+ store_token(token)
102
+
103
+ Token.new(token)
104
+ end
105
+
106
+ def get_stripe_token(name_on_card, number, exp_month, exp_year, cvc)
107
+ Stripe::Token.create(
108
+ :card => {
109
+ :name => name_on_card,
110
+ :number => number.to_s,
111
+ :exp_month => exp_month.to_i,
112
+ :exp_year => exp_year.to_i,
113
+ :cvc => cvc.to_i
114
+ }
115
+ )
116
+ end
117
+
118
+ def login(username, password)
119
+ json = post("/login", { :username => username, :password => password })
120
+ token = json['token']
121
+ store_token(token)
122
+ Token.new(token)
123
+ end
124
+
125
+ def info
126
+ json = get("/info")
127
+ json
128
+ end
129
+
130
+ def logout
131
+ remove_token
132
+ end
133
+
134
+ def username_available?(username)
135
+ get("/users/#{username}")
136
+ false
137
+ rescue ResourceNotFound
138
+ true
139
+ end
140
+
141
+ def validate_domain(domain)
142
+ post("/site_available", {
143
+ :domain => domain
144
+ })
145
+ rescue Conflict
146
+ false
147
+ end
148
+
149
+ def delete_site(domain_name)
150
+ json = delete("/sites", :domain => domain_name)
151
+ !!json["site"]
152
+ end
153
+
154
+ def prepare_push(path, checksums=nil, domain_file="Fjordsfile")
155
+ raise BadPathError.new unless File.exists?(path) && File.directory?(path)
156
+
157
+ if checksums && checksums["site"] && checksums["site"]["existing_hashes"]
158
+ existing_hashes = checksums["site"]["existing_hashes"]
159
+ end
160
+
161
+ zipfile, changeless = zip!(path, domain_file, existing_hashes || nil)
162
+ upload_size = File.size(zipfile)
163
+
164
+ if upload_size > (1024*1024*50)
165
+ raise UploadTooLarge.new
166
+ end
167
+
168
+ if upload_size > 1024*1024
169
+ upload_size = (upload_size/(1024.0*1024.0)).ceil.to_s + 'M'
170
+ elsif upload_size > 0
171
+ upload_size = (upload_size/1024.0).ceil.to_s + 'K'
172
+ else
173
+ upload_size = '0K'
174
+ end
175
+
176
+ [zipfile, changeless, upload_size]
177
+ end
178
+
179
+ def push(zipfile, changeless=[], domain=nil, permanent=nil, &block)
180
+ raise BadPathError.new unless File.exists?(zipfile)
181
+
182
+ data = {
183
+ :site_data => File.new(zipfile, 'rb'),
184
+ :changeless => changeless
185
+ }
186
+ data[:domain] = domain if domain
187
+ data[:permanent] = permanent if permanent
188
+
189
+ json = post("/sites", data)
190
+
191
+ wait_until_complete(json['site'], &block)
192
+ end
193
+
194
+ def wait_until_complete(site, &block)
195
+ sleep(3)
196
+
197
+ json = get(site["url"])
198
+
199
+ block.call(json['site']) if block_given?
200
+
201
+ state = json['site']['state']
202
+
203
+ if state != "live"
204
+ wait_until_complete(site, &block)
205
+ else
206
+ json
207
+ end
208
+ end
209
+
210
+ def sites
211
+ json = get("/sites")
212
+ json['sites']
213
+ end
214
+
215
+ def bugreport(body, attach=nil)
216
+ params = { :body => body }
217
+ if attach
218
+ params[:attach] = File.new(attach, 'r')
219
+ end
220
+
221
+ json = post("/bugreport", params)
222
+ json['bugreport']
223
+ end
224
+
225
+ private
226
+
227
+ def get_file_paths(path)
228
+ Dir.glob("#{path}/**/*").reject do |f|
229
+ !File.file?(f) || EXCLUSION_GLOBS.any? do |e|
230
+ File.fnmatch(e, File.basename(f))
231
+ end
232
+ end
233
+ end
234
+
235
+ def zip!(path, domain_file, existing_hashes=nil)
236
+ excludes = EXCLUSION_GLOBS.dup
237
+ excludes << "#{domain_file}"
238
+ excludes << "*/#{domain_file}"
239
+ zipfile = File.expand_path(File.join(Dir.tmpdir, "pack-#{Time.now.to_i}.zip"))
240
+ changeless = []
241
+
242
+ if existing_hashes
243
+ all_files = get_file_paths(path)
244
+ unique_files = all_files.dup
245
+
246
+ all_files.each do |p|
247
+ relative_path = Pathname(p).relative_path_from(Pathname(path)).to_s
248
+ if relative_path == domain_file
249
+ unique_files.delete(p)
250
+ elsif existing_hashes[relative_path] == get_md5(p)
251
+ excludes << relative_path
252
+ changeless << relative_path
253
+ unique_files.delete(p)
254
+ end
255
+ end
256
+
257
+ if unique_files.length <= 0
258
+ raise NothingToUpload
259
+ end
260
+ end
261
+
262
+ zip_excludes = excludes.map { |e| "-x \"#{e}\"" }.join(' ')
263
+
264
+ Dir.chdir(path) do
265
+ puts `zip -y -q -r #{zipfile} . #{zip_excludes}`
266
+ end
267
+
268
+ [zipfile, changeless]
269
+ end
270
+
271
+ def require_login!
272
+ raise NotAuthorizedError unless @user || logged_in?
273
+ end
274
+
275
+ def path(resource_name)
276
+ "#{self.class.host}#{resource_name}"
277
+ end
278
+
279
+ def get_md5(path)
280
+ digest = ::Digest::MD5.new
281
+
282
+ ::File.open(path, 'rb') do |stream|
283
+ buffer = ""
284
+ digest.update(buffer) while stream.read(4096, buffer)
285
+ end
286
+
287
+ digest.to_s
288
+ end
289
+
290
+ def get(resource_path, extra_headers={})
291
+ request(:get, resource_path, nil, extra_headers)
292
+ end
293
+
294
+ def post(resource_path, params={}, extra_headers={})
295
+ request(:post, resource_path, params, extra_headers)
296
+ end
297
+
298
+ def delete(resource_path, params={}, extra_headers={})
299
+ request(:delete, resource_path, params, extra_headers)
300
+ end
301
+
302
+ def safe_url(url)
303
+ url = path(url)
304
+ Addressable::URI.parse(url).normalize.to_str
305
+ end
306
+
307
+ def request(type, url, params=nil, extra_headers={})
308
+ headers = {
309
+ :accept => :json
310
+ }
311
+
312
+ token = self.logged_in?
313
+ headers[:authorization] = %Q{Token token="#{token.token}"} if token
314
+
315
+ headers.merge!(extra_headers)
316
+
317
+ req = {
318
+ :method => type.to_sym,
319
+ :url => safe_url(url),
320
+ :headers => headers
321
+ }
322
+ req[:payload] = params unless params.nil?
323
+
324
+ begin
325
+ response = RestClient::Request.execute(req)
326
+ headers[:accept] == :json ? JSON.parse(response) : response
327
+ rescue RestClient::InternalServerError => e
328
+ raise InternalServerError.new(e)
329
+ rescue RestClient::ServerBrokeConnection => e
330
+ raise InternalServerError.new(e)
331
+ rescue RestClient::ResourceNotFound => e
332
+ raise ResourceNotFound.new(e)
333
+ rescue RestClient::Unauthorized => e
334
+ raise LoginInvalid.new(e)
335
+ rescue RestClient::Conflict => e
336
+ raise Conflict.new(e)
337
+ rescue RestClient::Forbidden => e
338
+ raise Forbidden.new(e)
339
+ rescue Errno::ECONNREFUSED => e
340
+ raise ConnectionRefused.new(e)
341
+ rescue Exception => e
342
+ raise ConnectionRefused.new(e)
343
+ end
344
+ end
345
+
346
+ TOKEN_FILE_PATH = "~/.fjords_token"
347
+
348
+ def read_token
349
+ return @token if @token
350
+
351
+ token_file = File.expand_path(TOKEN_FILE_PATH)
352
+
353
+ return nil unless File.exists?(token_file)
354
+
355
+ @token = Token.new(read_file(token_file).strip)
356
+ end
357
+
358
+ def remove_token
359
+ path = File.expand_path(TOKEN_FILE_PATH)
360
+ if File.exists?(path)
361
+ FileUtils.rm(path)
362
+ end
363
+ end
364
+
365
+ def store_token(token)
366
+ token_file = File.expand_path(TOKEN_FILE_PATH)
367
+ write_file(token_file, token)
368
+ read_token
369
+ end
370
+
371
+ def read_file(file)
372
+ File.open(file, File::RDONLY) {|f|
373
+ f.flock(File::LOCK_EX)
374
+ contents = f.read
375
+ f.flock(File::LOCK_UN)
376
+ contents
377
+ }
378
+ end
379
+
380
+ def write_file(file, contents)
381
+ File.open(file, File::RDWR | File::CREAT, 0600) {|f|
382
+ f.flock(File::LOCK_EX)
383
+ f.rewind
384
+ f.puts contents
385
+ f.flush
386
+ f.truncate(f.pos)
387
+ f.flock(File::LOCK_UN)
388
+ }
389
+ end
390
+ end
391
+ end
@@ -0,0 +1,5 @@
1
+ module Fjords
2
+ class Client
3
+ VERSION = "0.0.6"
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ FJORDS_CLIENT_ROOT = File.join(File.expand_path(File.dirname(__FILE__)), 'fjords-client')
2
+
3
+ require "#{FJORDS_CLIENT_ROOT}/fjords-client"
4
+ require "#{FJORDS_CLIENT_ROOT}/version"
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
+
4
+ describe "Site listing" do
5
+ before(:each) do
6
+ stub_request(:post, "https://api.fjords.cc/v1/users/testy/login").
7
+ with(:body => {"password"=>"wrong"}).
8
+ to_return(:status => 401, :body => "")
9
+
10
+ stub_request(:post, "https://api.fjords.cc/v1/users/testy/login").
11
+ with(:body => {"password"=>""}).
12
+ to_return(:status => 401, :body => "")
13
+
14
+ stub_request(:post, "https://api.fjords.cc/v1/users/testy/login").
15
+ with(:body => {"password"=>"mcgee"}).
16
+ to_return(:status => 200, :body => {
17
+ :account => { :username => "testy" },
18
+ :token => "tokenkey"
19
+ }.to_json)
20
+ end
21
+
22
+ it "should be false when not logged in" do
23
+ Fjords::Client.logged_in?.should be_false
24
+ end
25
+
26
+ it "should catch login invalid error with missing password" do
27
+ expect {
28
+ Fjords::Client.login("testy", "")
29
+ }.to raise_error(Fjords::Client::LoginInvalid)
30
+ WebMock.should have_requested(:post, "https://api.fjords.cc/v1/users/testy/login").with({ :body =>{"password"=>""} })
31
+ end
32
+
33
+ it "should catch login invalid error with wrong password" do
34
+ expect {
35
+ Fjords::Client.login("testy", "wrong")
36
+ }.to raise_error(Fjords::Client::LoginInvalid)
37
+ WebMock.should have_requested(:post, "https://api.fjords.cc/v1/users/testy/login").with({ :body =>{"password"=>"wrong"} })
38
+ end
39
+
40
+ it "should be true when a token exists" do
41
+ Fjords::Client.login("testy", "mcgee")
42
+ WebMock.should have_requested(:post, "https://api.fjords.cc/v1/users/testy/login").with({ :body =>{"password"=>"mcgee"} })
43
+
44
+ Fjords::Client.logged_in?.should be_true
45
+ end
46
+ end
@@ -0,0 +1,16 @@
1
+ require 'rspec'
2
+ require 'webmock/rspec'
3
+
4
+ $: << File.join(File.dirname(__FILE__), '..', 'lib', 'fjords-client')
5
+
6
+ require 'fjords-client'
7
+
8
+ RSpec.configure do |conf|
9
+ conf.include WebMock::API
10
+ conf.include WebMock::Matchers
11
+
12
+ conf.before(:all) do
13
+ Fjords::Client.logout
14
+ # Fjords::Client.host = "https://fake.fjords.cc/v1"
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fjords-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.6
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Thomas Reynolds
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rest-client
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.6.7
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.6.7
30
+ - !ruby/object:Gem::Dependency
31
+ name: addressable
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '='
36
+ - !ruby/object:Gem::Version
37
+ version: 2.3.2
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '='
44
+ - !ruby/object:Gem::Version
45
+ version: 2.3.2
46
+ - !ruby/object:Gem::Dependency
47
+ name: stripe
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: Fjords.cc API
63
+ email:
64
+ - me@tdreyno.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - Rakefile
72
+ - fjords-client.gemspec
73
+ - lib/fjords-client.rb
74
+ - lib/fjords-client/fjords-client.rb
75
+ - lib/fjords-client/version.rb
76
+ - spec/logged_in_spec.rb
77
+ - spec/spec_helper.rb
78
+ homepage: ''
79
+ licenses: []
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ segments:
91
+ - 0
92
+ hash: 1471701448195210841
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ segments:
100
+ - 0
101
+ hash: 1471701448195210841
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 1.8.23
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: Fjords.cc API
108
+ test_files:
109
+ - spec/logged_in_spec.rb
110
+ - spec/spec_helper.rb