hubic 0.0.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.
data/.gitignore ADDED
@@ -0,0 +1,34 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+
12
+ ## Specific to RubyMotion:
13
+ .dat*
14
+ .repl_history
15
+ build/
16
+
17
+ ## Documentation cache and generated files:
18
+ /.yardoc/
19
+ /_yardoc/
20
+ /doc/
21
+ /rdoc/
22
+
23
+ ## Environment normalisation:
24
+ /.bundle/
25
+ /lib/bundler/man/
26
+
27
+ # for a library or gem, you might want to ignore these files since the code is
28
+ # intended to run in multiple environments; otherwise, check them in:
29
+ # Gemfile.lock
30
+ # .ruby-version
31
+ # .ruby-gemset
32
+
33
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
34
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # -*- ruby -*-
2
+
3
+ # Could be useful to fix permission error during local installation
4
+ ENV['RB_USER_INSTALL'] = 'true'
5
+ ENV['NOKOGIRI_USE_SYSTEM_LIBRARIES'] = "1"
6
+
7
+ source "http://rubygems.org"
8
+
9
+ gemspec
10
+
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 sdalu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ Hubic
2
+ =====
3
+
4
+ Requirement
5
+ -----------
6
+ You need to retrieve the client id, secret key, and redirection url.
7
+ For that if not alredy done you need to register an application into your account.
8
+ To start the registration process you will need to go to ``My Account``, select ``Your application``, and click on ``Add an application``.
9
+
10
+ Quick example
11
+ -------------
12
+ ```ruby
13
+ require 'hubic'
14
+
15
+ # Configure the client
16
+ Hubic.default_redirect_uri = '** your redirect_uri **'
17
+ Hubic.default_client_id = '** your client_id **'
18
+ Hubic.default_client_secret = '** your client_secret **'
19
+
20
+ # Create a hubic handler for the desired user
21
+ h = Hubic.for_user('** your login **', '** your password **')
22
+
23
+ # Download file hubic-foo.txt and save it to local-foo.txt
24
+ h.download("hubic-foo.txt", "local-foo.txt")
25
+
26
+ # Upload file local-foo.txt and save it to hubic with the name hubic-bar.txt
27
+ h.upload("local-foo.txt", "hubic-bar.txt")
28
+ ```
29
+
30
+
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # -*- ruby -*-
2
+
3
+ require "rbconfig"
4
+ require "rubygems"
5
+ require "bundler/setup"
6
+
7
+ include Rake::DSL
8
+
9
+
10
+ Bundler::GemHelper.install_tasks
11
+
data/hubic.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.unshift File.expand_path("../lib", __FILE__)
3
+ require "hubic/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "hubic"
7
+ s.version = Hubic::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = [ "Stephane D'Alu" ]
10
+ s.email = ["sdalu@sdalu.com" ]
11
+ s.homepage = "http://github.com/sdalu/ruby-hubic"
12
+ s.summary = "Manage your Hubic account from Ruby"
13
+ s.description = "Manage your Hubic account from Ruby"
14
+
15
+ s.add_dependency "faraday", "~>0.9"
16
+ s.add_dependency "nokogiri"
17
+ s.add_development_dependency "rake"
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
21
+ s.require_path = 'lib'
22
+ end
data/lib/hubic.rb ADDED
@@ -0,0 +1,219 @@
1
+ # https://api.hubic.com/docs/#/server-side
2
+ # http://docs.openstack.org/api/openstack-object-storage/1.0/content/authentication-examples-curl.html
3
+
4
+
5
+ require 'pathname'
6
+ require 'uri'
7
+ require 'time'
8
+
9
+ require 'json'
10
+ require 'faraday'
11
+ require 'nokogiri'
12
+
13
+ # TODO
14
+ # - verify scope (requested == granted)
15
+ # - better scope handling
16
+ # - deal with state string
17
+ # - refresh token and credentials
18
+
19
+ # X-Storage-Url
20
+ # X-Auth-Token
21
+
22
+ require_relative 'hubic/version'
23
+ require_relative 'hubic/store'
24
+ require_relative 'hubic/openstack'
25
+ require_relative 'hubic/file_ops'
26
+
27
+ class Hubic
28
+ class Error < StandardError
29
+ class Auth < Error
30
+ end
31
+ class NotFound < Error
32
+ end
33
+ end
34
+
35
+
36
+ def self.default_client_id=(client_id)
37
+ @@client_id = client_id
38
+ end
39
+
40
+ def self.default_client_secret=(client_secret)
41
+ @@client_secret = client_secret
42
+ end
43
+
44
+ def self.default_redirect_uri=(redirect_uri)
45
+ @@redirect_uri = redirect_uri
46
+ end
47
+
48
+
49
+ def self.for_user(user, password=nil, store: Store[user],
50
+ &password_requester)
51
+ h = Hubic.new(@@client_id, @@client_secret, @@redirect_uri)
52
+ h.for_user(user, password, store: store, &password_requester)
53
+ h
54
+ end
55
+
56
+
57
+ def initialize(client_id = @@client_id,
58
+ client_secret = @@client_secret,
59
+ redirect_uri = @@redirect_uri)
60
+ @store = nil
61
+ @client_id = client_id
62
+ @client_secret = client_secret
63
+ @redirect_uri = redirect_uri
64
+ @conn = Faraday.new('https://api.hubic.com') do |faraday|
65
+ faraday.request :url_encoded
66
+ faraday.adapter :net_http
67
+ faraday.options.params_encoder = Faraday::FlatParamsEncoder
68
+ end
69
+ @default_container = "default"
70
+ end
71
+
72
+ def for_user(user, password=nil, store: Store[user], &password_requester)
73
+ @store = store
74
+ @refresh_token = @store['refresh_token'] if @store
75
+
76
+ if @refresh_token
77
+ data = refresh_access_token
78
+ @access_token = data[:access_token]
79
+ @expires_at = data[:expires_at ]
80
+ else
81
+ password ||= password_requester.call(user) if password_requester
82
+ if password.nil?
83
+ raise ArgumentError, "password requiered for user authorization"
84
+ end
85
+ code = get_request_code(user, password)
86
+ data = get_access_token(code)
87
+ @access_token = data[:access_token ]
88
+ @expires_at = data[:expires_in ]
89
+ @refresh_token = data[:refresh_token]
90
+ if @store
91
+ @store['refresh_token'] = @refresh_token
92
+ @store.save
93
+ end
94
+ end
95
+ end
96
+
97
+
98
+ def account
99
+ api_hubic(:get, '/1.0/account')
100
+ end
101
+
102
+ def credentials
103
+ api_hubic(:get, '/1.0/account/credentials')
104
+ end
105
+
106
+
107
+
108
+ def [](path=nil, container=@default_container)
109
+ # objects(path, container: container)
110
+ end
111
+
112
+
113
+
114
+ def api_hubic(method, path, params=nil)
115
+ r = @conn.method(method).call(path) do |req|
116
+ req.headers['Authorization'] = "Bearer #{@access_token}"
117
+ req.params = params if params
118
+ end
119
+ JSON.parse(r.body)
120
+ end
121
+
122
+ private
123
+
124
+
125
+ def get_request_code(user, password)
126
+ # Request code (retrieve user confirmation form)
127
+ r = @conn.get '/oauth/auth', {
128
+ :client_id => @client_id,
129
+ :response_type => 'code',
130
+ :redirect_uri => 'http://localhost/',
131
+ :scope => 'account.r,usage.r,links.drw,credentials.r',
132
+ :state => 'random'
133
+ }
134
+
135
+ # Autofill confirmation
136
+ params = {}
137
+ doc = Nokogiri::HTML(r.body)
138
+ doc.css('input').each {|i|
139
+ case i[:name]
140
+ when 'login'
141
+ params[:login ] = user
142
+ next
143
+ when 'user_pwd'
144
+ params[:user_pwd] = password
145
+ next
146
+ end
147
+
148
+ case i[:type]
149
+ when 'checkbox', 'hidden', 'text'
150
+ (params[i[:name]] ||= []) << i[:value] if i[:name]
151
+ end
152
+ }
153
+ if params.empty?
154
+ raise Error, "unable to autofill confirmation form"
155
+ end
156
+
157
+ # Confirm and get code
158
+ r = @conn.post '/oauth/auth', params
159
+ q = Hash[URI.decode_www_form(URI(r[:location]).query)]
160
+
161
+ case r.status
162
+ when 302
163
+ q['code']
164
+ when 400, 401, 500
165
+ raise Error::Auth, "#{q['error']} #{q['error_description']}"
166
+ else
167
+ raise Error::Auth, "unhandled response code (#{r.status})"
168
+ end
169
+ end
170
+
171
+ def get_access_token(code)
172
+ r = @conn.post '/oauth/token', {
173
+ :code => code,
174
+ :redirect_uri => 'http://localhost/',
175
+ :grant_type => 'authorization_code',
176
+ :client_id => @client_id,
177
+ :client_secret => @client_secret
178
+ }
179
+ j = JSON.parse(r.body)
180
+ case r.status
181
+ when 200
182
+ { :acces_token => j['access_token'],
183
+ :expires_at => Time.parse(r[:date]) + j['expires_in'].to_i,
184
+ :refresh_token => j['refresh_token']
185
+ }
186
+ when 400, 401, 500
187
+ raise Error::Auth, "#{j['error']} #{j['error_description']}"
188
+ else
189
+ raise Error::Auth, "unhandled response code (#{r.status})"
190
+ end
191
+ end
192
+
193
+ def refresh_access_token
194
+ if @refresh_token.nil?
195
+ raise Error, "refresh_token was not previously acquiered"
196
+ end
197
+ r = @conn.post '/oauth/token', {
198
+ :refresh_token => @refresh_token,
199
+ :grant_type => 'refresh_token',
200
+ :client_id => @client_id,
201
+ :client_secret => @client_secret
202
+ }
203
+ j = JSON.parse(r.body)
204
+ case r.status
205
+ when 200
206
+ { :access_token => j['access_token'],
207
+ :expires_at => Time.parse(r[:date]) + j['expires_in'].to_i
208
+ }
209
+ when 400, 401, 500
210
+ raise Error::Auth, "#{j['error']} #{j['error_description']}"
211
+ else
212
+ raise Error::Auth, "unhandled response code (#{r.status})"
213
+ end
214
+ end
215
+
216
+ end
217
+
218
+
219
+
@@ -0,0 +1,54 @@
1
+ class Hubic
2
+
3
+ # File operations
4
+ #######################################################################
5
+
6
+ def download(obj, dst=nil, size: nil, offset: 0, &block)
7
+ io = nil
8
+ dst = Pathname(dst) if String === dst
9
+ _dst = case dst
10
+ when Pathname then io = dst.open('w')
11
+ when NilClass then io = StringIO.new
12
+ else dst
13
+ end
14
+
15
+ meta = get_object(obj, _dst, size: size, offset: offset, &block)
16
+
17
+ if (Pathname === dst) && meta[:lastmod]
18
+ dst.utime(meta[:lastmod], meta[:lastmod])
19
+ end
20
+
21
+ if dst.nil?
22
+ then io.flush.string
23
+ else meta
24
+ end
25
+ ensure
26
+ io.close unless io.nil?
27
+ end
28
+
29
+ def upload(src, obj, &block)
30
+ put_object(src, obj, &block)
31
+ end
32
+
33
+ def copy(src, dst)
34
+ raise "not implemented yet"
35
+ end
36
+
37
+ def move(src, dst)
38
+ raise "not implemented yet"
39
+ end
40
+
41
+ def delete(src)
42
+ raise "not implemented yet"
43
+ end
44
+
45
+ def checksum(src)
46
+ raise "not implemented yet"
47
+ end
48
+
49
+
50
+ def list(path = '/', container = @default_container)
51
+ objects(container, path: path)
52
+ end
53
+
54
+ end
@@ -0,0 +1,222 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'json'
5
+
6
+
7
+ class Hubic
8
+ class Container
9
+ attr_reader :name, :count, :bytes, :metadata
10
+ def initialize(hubic, name)
11
+ @hubic = hubic
12
+ @name = name.dup.freeze
13
+ j, h = @hubic.api_openstack(:head, @name)
14
+ @count = h['x-container-object-count'].to_i
15
+ @bytes = h['x-container-bytes-used' ].to_i
16
+ @etag = h['etag' ]
17
+ @metadata = Hash[h.map {|k,v|
18
+ if k =~ /^x-container-meta-(.*)/
19
+ [ $1, v ]
20
+ end
21
+ }.compact].freeze
22
+ end
23
+
24
+ def [](key)
25
+ @metadata[key]
26
+ end
27
+
28
+ def empty?
29
+ @count == 0
30
+ end
31
+
32
+ def destroy!
33
+ j, h = @hubic.api_openstack(:delete, @name)
34
+ j
35
+ end
36
+
37
+ def object(path)
38
+ end
39
+
40
+ def objects()
41
+ end
42
+
43
+
44
+ end
45
+
46
+ class Object
47
+ def initialize(hubic)
48
+ @hubic = hubic
49
+ end
50
+ end
51
+
52
+ def get_object(obj, dst=nil, size: nil, offset: 0, &block)
53
+ container, path, uri = normalize_object(obj)
54
+
55
+ if !(IO === dst) && !dst.respond_to?(:write) &&
56
+ !(Proc === dst) && !dst.respond_to?(:call)
57
+ raise ArgumentError, "unsupported destination"
58
+ end
59
+
60
+ meta = {}
61
+
62
+ hdrs = {}
63
+ hdrs['X-Auth-Token'] = @os[:token]
64
+
65
+ if size
66
+ hdrs['Range'] = sprintf("bytes=%d-%d", offset, offset + size - 1)
67
+ end
68
+
69
+ http = Net::HTTP.new(uri.host, uri.port)
70
+ if uri.scheme == 'https'
71
+ http.use_ssl = true
72
+ # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
73
+ end
74
+ http.start
75
+
76
+
77
+ http.request_get(uri.request_uri, hdrs) {|response|
78
+ case response
79
+ when Net::HTTPSuccess
80
+ when Net::HTTPRedirection
81
+ fail "http redirect is not currently handled"
82
+ when Net::HTTPUnauthorized
83
+ # TODO: Need to refresh token
84
+ else
85
+ fail "resource unavailable: #{uri} (#{response.class})"
86
+ end
87
+
88
+ lastmod = Time.parse(response['last-modified']) rescue nil
89
+ length = response.content_length
90
+ type = response['content-type']
91
+ meta = { :lastmod => lastmod,
92
+ :length => length,
93
+ :type => type
94
+ }
95
+
96
+ if block
97
+ block.call(meta)
98
+ end
99
+
100
+ response.read_body {|segment|
101
+ if IO === dst then dst.write(segment)
102
+ elsif Proc === dst then dst.call(segment)
103
+ elsif dst.respond_to?(:write) then dst.write(segment)
104
+ elsif dst.respond_to?(:call ) then dst.call(segment)
105
+ end
106
+
107
+ if block
108
+ block.call(segment)
109
+ end
110
+ }
111
+ }
112
+ if block
113
+ block.call(:done)
114
+ end
115
+
116
+ meta
117
+ ensure
118
+ http.finish unless http.nil?
119
+ end
120
+
121
+ def put_object(src, obj, &block)
122
+ container, path, uri = normalize_object(obj)
123
+
124
+ r = @conn.put(uri) do |req|
125
+ req.headers['X-Auth-Token'] = @os[:token]
126
+ req.params[:file] = Faraday::UploadIO.new(src, 'text/plain')
127
+ end
128
+ puts r.inspect
129
+ end
130
+
131
+ def objects(container = @default_container,
132
+ path: nil, limit: nil, gt: nil, lt: nil)
133
+ path = path[1..-1] if path && path[0] == ?/
134
+ p = { path: path, limit: limit, marker: gt, end_marker: lt
135
+ }.delete_if {|k,v| v.nil? }
136
+ j, = api_openstack(:get, container, p)
137
+ Hash[j.map {|o| [ o['name'], {
138
+ :hash => o['hash'],
139
+ :lastmod => Time.parse(o['last_modified']),
140
+ :size => o['bytes'].to_i,
141
+ :type => o['content_type']
142
+ } ] } ]
143
+ end
144
+
145
+ def containers
146
+ j, = api_openstack(:get, '/')
147
+ Hash[j.map {|c| [ c['name'], { :size => c['bytes'].to_i,
148
+ :count => c['count'].to_i } ] } ]
149
+ end
150
+
151
+ def container(name)
152
+ Container.new(self, name)
153
+ end
154
+
155
+ def default_container=(name)
156
+ @default_container = name
157
+ end
158
+
159
+ def api_openstack(method, path, params=nil)
160
+ openstack_setup_refresh
161
+
162
+ params ||= {}
163
+ params[:format] ||= :json
164
+
165
+ p = "#{@os[:endpoint]}#{'/' if path[0] != ?/}#{path}"
166
+ r = @conn.method(method).call(p) do |req|
167
+ req.headers['X-Auth-Token'] = @os[:token]
168
+ req.params = params
169
+ end
170
+
171
+ if r.body.nil? || r.body.empty?
172
+ then [ nil, r.headers ]
173
+ else [ JSON.parse(r.body), r.headers ]
174
+ end
175
+ end
176
+
177
+
178
+ def openstack_setup_refresh(force: false)
179
+ return unless force || @os.nil? || @os[:expires] <= Time.now
180
+
181
+ data = self.credentials
182
+ endpoint = data['endpoint']
183
+ token = data['token']
184
+ expires = Time.parse(data['expires'])
185
+
186
+ openstack_setup(endpoint, token, expires)
187
+ end
188
+
189
+ def openstack_setup(endpoint, token, expires)
190
+ conn = Faraday.new do |faraday|
191
+ faraday.request :url_encoded
192
+ faraday.adapter :net_http
193
+ faraday.options.params_encoder = Faraday::FlatParamsEncoder
194
+ end
195
+ @os = {
196
+ :expires => expires,
197
+ :token => token,
198
+ :endpoint => endpoint,
199
+ :conn => conn
200
+ }
201
+ end
202
+
203
+ def normalize_object(obj)
204
+ c, p = case obj
205
+ when String
206
+ [ @default_container, obj ]
207
+ when Hash
208
+ [ obj[:name] || obj[:path],
209
+ (obj[:container] || @default_container).to_s ]
210
+ when Array
211
+ case obj.length
212
+ when 1 then [ @default_container, obj ]
213
+ when 2 then Symbol === obj[1] ? [ obj[1], obj[0] ] : obj
214
+ else raise ArguementError
215
+ end
216
+ end
217
+ c = c.to_s
218
+ p = p[1..-1] if p[0] == ?/
219
+ [ c, p, URI("#{@os[:endpoint]}/#{c}/#{p}") ]
220
+ end
221
+
222
+ end
@@ -0,0 +1,40 @@
1
+ require 'yaml'
2
+
3
+ class Hubic
4
+ class Store
5
+ FILE = "#{ENV['HOME']}/.hubic"
6
+
7
+ extend Forwardable
8
+ def_delegator :@data, :[]
9
+ def_delegator :@data, :[]=
10
+
11
+ def initialize(file = FILE, user = nil)
12
+ @file = file
13
+ @user = user
14
+ @data = Hash(begin
15
+ if data = YAML.load_file(@file)
16
+ @user.nil? ? data : data[@user]
17
+ end
18
+ rescue Errno::ENOENT
19
+ end)
20
+ end
21
+
22
+ def self.[](user)
23
+ self.new(file = FILE, user)
24
+ end
25
+
26
+ def save
27
+ data = if @user
28
+ ( begin
29
+ YAML.load_file(@file)
30
+ rescue Errno::ENOENT
31
+ end || {} ).merge(@user => @data).to_yaml
32
+ else
33
+ @data
34
+ end
35
+ File.open(@file, 'w', 0600) {|io|
36
+ io.write(data.to_yaml)
37
+ }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ class Hubic
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hubic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Stephane D'Alu
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-05-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: faraday
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '0.9'
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: '0.9'
30
+ - !ruby/object:Gem::Dependency
31
+ name: nokogiri
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
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: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
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: Manage your Hubic account from Ruby
63
+ email:
64
+ - sdalu@sdalu.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - LICENSE
72
+ - README.md
73
+ - Rakefile
74
+ - hubic.gemspec
75
+ - lib/hubic.rb
76
+ - lib/hubic/file_ops.rb
77
+ - lib/hubic/openstack.rb
78
+ - lib/hubic/store.rb
79
+ - lib/hubic/version.rb
80
+ homepage: http://github.com/sdalu/ruby-hubic
81
+ licenses: []
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - '>='
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ segments:
93
+ - 0
94
+ hash: 4363560357882864147
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - '>='
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ segments:
102
+ - 0
103
+ hash: 4363560357882864147
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 1.8.29
107
+ signing_key:
108
+ specification_version: 3
109
+ summary: Manage your Hubic account from Ruby
110
+ test_files: []