sndacs 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -20,7 +20,6 @@ GEM
20
20
  rspec-expectations (2.9.0)
21
21
  diff-lcs (~> 1.1.3)
22
22
  rspec-mocks (2.9.0)
23
- test-unit (2.4.8)
24
23
 
25
24
  PLATFORMS
26
25
  ruby
@@ -30,4 +29,3 @@ DEPENDENCIES
30
29
  mocha
31
30
  rspec (~> 2.0)
32
31
  sndacs!
33
- test-unit (>= 2.0)
data/README.rdoc CHANGED
@@ -13,8 +13,26 @@ bases on the s3 gem: https://github.com/qoobaa/s3
13
13
  === Initialize the service
14
14
 
15
15
  require "sndacs"
16
- service = Sndacs::Service.new(:access_key_id => "...",
17
- :secret_access_key => "...")
16
+
17
+ service = Sndacs::Service.new(:access_key_id => '...'
18
+ :secret_access_key => '...')
19
+ #=> #<Sndacs::Service:...>
20
+
21
+ or use even more flexible config style
22
+
23
+ require "sndacs"
24
+
25
+ # Setup global default configurations, see Sndacs::Config for more info
26
+ Config.access_key_id = '...'
27
+ Config.secret_access_key = '...'
28
+ Config.host = 'storage.grandcloud.cn'
29
+ Config.content_host = 'storage.sdcloud.cn'
30
+ Config.proxy = nil
31
+ Config.timeout = 60
32
+ Config.use_ssl = false
33
+ Config.chunk_size = 1048576
34
+ Config.debug = false
35
+ service = Sndacs::Service.new
18
36
  #=> #<Sndacs::Service:...>
19
37
 
20
38
  === List buckets
@@ -64,7 +82,7 @@ bases on the s3 gem: https://github.com/qoobaa/s3
64
82
  new_object.save
65
83
  #=> true
66
84
 
67
- Please note that new objects are created with "public-read" ACL by
85
+ Please note that new objects are created with "private" ACL by
68
86
  default.
69
87
 
70
88
  == See also
data/lib/sndacs/bucket.rb CHANGED
@@ -1,83 +1,101 @@
1
1
  module Sndacs
2
+
2
3
  class Bucket
3
4
  include Parser
4
5
  include Proxies
5
6
  extend Forwardable
6
7
 
7
- attr_reader :name, :service
8
+ attr_reader :name, :location, :service
8
9
 
9
10
  def_instance_delegators :service, :service_request
10
11
  private_class_method :new
11
12
 
13
+ # Compares the bucket with other bucket. Returns true if the names
14
+ # of the buckets are the same, and both have the same locations and services
15
+ # (see Service equality)
16
+ def ==(other)
17
+ self.name == other.name and self.location == other.location and self.service == other.service
18
+ end
19
+
12
20
  # Retrieves the bucket information from the server. Raises an
13
- # S3::Error exception if the bucket doesn't exist or you don't
21
+ # Sndacs::Error exception if the bucket doesn't exist or you don't
14
22
  # have access to it, etc.
15
23
  def retrieve
16
24
  bucket_headers
25
+
17
26
  self
18
27
  end
19
28
 
20
- # Returns location of the bucket, e.g. "EU"
29
+ # Returns location of the bucket, e.g. "huabei-1"
21
30
  def location(reload = false)
22
31
  return @location if defined?(@location) and not reload
32
+
23
33
  @location = location_constraint
24
34
  end
25
35
 
26
- # Compares the bucket with other bucket. Returns true if the names
27
- # of the buckets are the same, and both have the same services
28
- # (see Service equality)
29
- def ==(other)
30
- self.name == other.name and self.service == other.service
31
- end
36
+ # Saves the newly built bucket. Raises Sndacs::Error::BucketAlreadyExists
37
+ # exception if the bucket already exists.
38
+ #
39
+ # ==== Options
40
+ # * <tt>:location</tt> - location of the bucket
41
+ # (<tt>huabei-1</tt> or <tt>huadong-1</tt>)
42
+ # * Any other options are passed through to Connection#request
43
+ def save(options = {})
44
+ if options
45
+ if options.is_a?(String) && options.strip != ''
46
+ options = {:location => options.strip}
47
+ end
48
+
49
+ if options.is_a?(Hash) && !options.has_key?(:location)
50
+ options.merge!(:location => location)
51
+ end
52
+ else
53
+ options = {:location => location}
54
+ end
55
+
56
+ create_bucket_configuration(options)
32
57
 
33
- # Similar to retrieve, but catches S3::Error::NoSuchBucket
34
- # exceptions and returns false instead.
35
- def exists?
36
- retrieve
37
58
  true
38
- rescue Error::NoSuchBucket
39
- false
40
59
  end
41
60
 
42
- # Destroys given bucket. Raises an S3::Error::BucketNotEmpty
43
- # exception if the bucket is not empty. You can destroy non-empty
44
- # bucket passing true (to force destroy)
61
+ # Destroys given bucket. Raises an Sndacs::Error::BucketNotEmpty
62
+ # exception if the bucket is not empty.
63
+ # You can destroy non-empty bucket passing true (to force destroy)
45
64
  def destroy(force = false)
46
65
  delete_bucket
66
+
47
67
  true
48
68
  rescue Error::BucketNotEmpty
49
69
  if force
50
70
  objects.destroy_all
71
+
51
72
  retry
52
73
  else
53
74
  raise
54
75
  end
55
76
  end
56
77
 
57
- # Saves the newly built bucket.
58
- #
59
- # ==== Options
60
- # * <tt>:location</tt> - location of the bucket
61
- # (<tt>:eu</tt> or <tt>us</tt>)
62
- # * Any other options are passed through to
63
- # Connection#request
64
- def save(options = {})
65
- options = {:location => options} unless options.is_a?(Hash)
66
- create_bucket_configuration(options)
78
+ # Similar to retrieve, but catches Sndacs::Error::NoSuchBucket
79
+ # exceptions and returns false instead.
80
+ def exists?
81
+ retrieve
82
+
67
83
  true
84
+ rescue Error::NoSuchBucket
85
+ false
68
86
  end
69
87
 
70
- # Returns true if the name of the bucket can be used like +VHOST+
71
- # name. If the bucket contains characters like underscore it can't
72
- # be used as +VHOST+ (e.g. <tt>bucket_name.s3.amazonaws.com</tt>)
88
+ # Returns true if the name of the bucket can be used like +VHOST+ name.
89
+ # If the bucket contains characters like underscore it can't
90
+ # be used as +VHOST+ (e.g. <tt>bucket_name.storage.grandcloud.cn</tt>)
73
91
  def vhost?
74
- #"#@name.#{HOST}" =~ /\A#{URI::REGEXP::PATTERN::HOSTNAME}\Z/
92
+ #"#@name.#{region_host}" =~ /\A#{URI::REGEXP::PATTERN::HOSTNAME}\Z/
75
93
  false
76
94
  end
77
95
 
78
96
  # Returns host name of the bucket according (see #vhost? method)
79
- def host
80
- vhost? ? "#@name.#{HOST}" : "#{HOST}"
97
+ def host(public_accessible = false)
98
+ vhost? ? "#@name.#{region_host(public_accessible)}" : region_host(public_accessible)
81
99
  end
82
100
 
83
101
  # Returns path prefix for non +VHOST+ bucket. Path prefix is used
@@ -102,19 +120,26 @@ module Sndacs
102
120
  "#<#{self.class}:#{name}>"
103
121
  end
104
122
 
105
- private
123
+ private
106
124
 
107
125
  attr_writer :service
108
126
 
109
- def location_constraint
110
- response = bucket_request(:get, :params => {:location => nil})
111
- parse_location_constraint(response.body)
127
+ def create_bucket_configuration(options = {})
128
+ location = options[:location].to_s.downcase if options[:location]
129
+
130
+ options[:headers] ||= {}
131
+ if location and location != REGION_DEFAULT
132
+ options[:headers][:content_type] = "application/xml"
133
+ options[:body] = "<CreateBucketConfiguration><LocationConstraint>#{location}</LocationConstraint></CreateBucketConfiguration>"
134
+ end
135
+
136
+ bucket_request(:put, options)
112
137
  end
113
138
 
114
139
  def list_bucket(options = {})
115
140
  response = bucket_request(:get, :params => options)
116
141
  max_keys = options[:max_keys]
117
- objects_attributes = parse_list_bucket_result(response.body)
142
+ objects_attributes = parse_all_objects_result(response.body)
118
143
 
119
144
  # If there are more than 1000 objects S3 truncates listing and
120
145
  # we need to request another listing for the remaining objects.
@@ -123,11 +148,12 @@ module Sndacs
123
148
 
124
149
  if max_keys
125
150
  break if objects_attributes.length >= max_keys
151
+
126
152
  next_request_options[:max_keys] = max_keys - objects_attributes.length
127
153
  end
128
154
 
129
155
  response = bucket_request(:get, :params => options.merge(next_request_options))
130
- objects_attributes += parse_list_bucket_result(response.body)
156
+ objects_attributes += parse_all_objects_result(response.body)
131
157
  end
132
158
 
133
159
  objects_attributes.map { |object_attributes| Object.send(:new, self, object_attributes) }
@@ -143,37 +169,59 @@ module Sndacs
143
169
  end
144
170
  end
145
171
 
146
- def create_bucket_configuration(options = {})
147
- location = options[:location].to_s.upcase if options[:location]
148
- options[:headers] ||= {}
149
- if location and location != "US"
150
- options[:body] = "<CreateBucketConfiguration><LocationConstraint>#{location}</LocationConstraint></CreateBucketConfiguration>"
151
- options[:headers][:content_type] = "application/xml"
152
- end
153
- bucket_request(:put, options)
172
+ def location_constraint
173
+ response = bucket_request(:get, :params => {:location => nil})
174
+
175
+ parse_location_constraint(response.body)
154
176
  end
155
177
 
156
178
  def delete_bucket
157
179
  bucket_request(:delete)
158
180
  end
159
181
 
160
- def initialize(service, name) #:nodoc:
182
+ def initialize(service, name, location = nil) #:nodoc:
161
183
  self.service = service
162
184
  self.name = name
185
+
186
+ unless location
187
+ begin
188
+ location = location_constraint
189
+ rescue
190
+ location = REGION_DEFAULT
191
+ end
192
+ end
193
+
194
+ self.location = location
163
195
  end
164
196
 
165
197
  def name=(name)
166
198
  raise ArgumentError.new("Invalid bucket name: #{name}") unless name_valid?(name)
199
+
167
200
  @name = name
168
201
  end
169
202
 
170
- def bucket_request(method, options = {})
171
- path = "#{path_prefix}#{options[:path]}"
172
- service_request(method, options.merge(:host => host, :path => path))
203
+ def location=(location)
204
+ @location = location
173
205
  end
174
206
 
175
207
  def name_valid?(name)
176
208
  name =~ /\A[a-z0-9][a-z0-9\._-]{2,254}\Z/i and name !~ /\A#{URI::REGEXP::PATTERN::IPV4ADDR}\Z/
177
209
  end
210
+
211
+ def region_host(public_accessible = false)
212
+ rhost = public_accessible ? Config.content_host : Config.host
213
+ if @location
214
+ rhost = (public_accessible ? REGION_CONTENT_HOST : REGION_HOST) % @location
215
+ end
216
+
217
+ rhost
218
+ end
219
+
220
+ def bucket_request(method, options = {})
221
+ path = "#{path_prefix}#{options[:path]}"
222
+
223
+ service_request(method, options.merge(:host => host, :path => path))
224
+ end
178
225
  end
226
+
179
227
  end
@@ -1,4 +1,5 @@
1
1
  module Sndacs
2
+
2
3
  module BucketsExtension
3
4
  # Builds new bucket with given name
4
5
  def build(name)
@@ -8,7 +9,10 @@ module Sndacs
8
9
  # Finds the bucket with given name
9
10
  def find_first(name)
10
11
  bucket = build(name)
12
+
11
13
  bucket.retrieve
14
+ rescue Error::NoSuchBucket, Error::ForbiddenBucket
15
+ nil
12
16
  end
13
17
  alias :find :find_first
14
18
 
@@ -23,4 +27,5 @@ module Sndacs
23
27
  proxy_target.each { |bucket| bucket.destroy(force) }
24
28
  end
25
29
  end
30
+
26
31
  end
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- encoding: utf-8 -*-
3
+
4
+ module Sndacs
5
+
6
+ class Config
7
+
8
+ class << self
9
+ attr_accessor :access_key_id
10
+ attr_accessor :secret_access_key
11
+
12
+ attr_accessor :proxy
13
+ attr_accessor :host
14
+ attr_accessor :content_host
15
+ attr_accessor :timeout
16
+ attr_accessor :use_ssl
17
+ attr_accessor :chunk_size
18
+ attr_accessor :debug
19
+
20
+ def access_key_id
21
+ @access_key_id ||= ''
22
+ end
23
+
24
+ def secret_access_key
25
+ @secret_access_key ||= ''
26
+ end
27
+
28
+ def proxy
29
+ @proxy ||= nil
30
+ end
31
+
32
+ def host
33
+ @host ||= 'storage.grandcloud.cn'
34
+ end
35
+
36
+ def content_host
37
+ @content_host ||= 'storage.sdcloud.cn'
38
+ end
39
+
40
+ def timeout
41
+ @timeout ||= 60
42
+ end
43
+
44
+ def use_ssl
45
+ @use_ssl ||= false
46
+ end
47
+
48
+ def chunk_size
49
+ @chunk_size ||= 1048576
50
+ end
51
+
52
+ def debug
53
+ @debug ||= false
54
+ end
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -1,7 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- encoding: utf-8 -*-
3
+
1
4
  module Sndacs
2
5
 
3
- # Class responsible for handling connections to amazon hosts
6
+ # Class responsible for handling connections to grandcloud hosts
4
7
  class Connection
8
+
5
9
  include Parser
6
10
 
7
11
  attr_accessor :access_key_id, :secret_access_key, :use_ssl, :timeout, :debug, :proxy
@@ -24,13 +28,13 @@ module Sndacs
24
28
  # * <tt>:chunk_size</tt> - Size of a chunk when streaming
25
29
  # (1048576 (1 MiB) by default)
26
30
  def initialize(options = {})
27
- @access_key_id = options.fetch(:access_key_id)
28
- @secret_access_key = options.fetch(:secret_access_key)
29
- @use_ssl = options.fetch(:use_ssl, false)
30
- @debug = options.fetch(:debug, false)
31
- @timeout = options.fetch(:timeout, 60)
32
- @proxy = options.fetch(:proxy, nil)
33
- @chunk_size = options.fetch(:chunk_size, 1048576)
31
+ @access_key_id = options.fetch(:access_key_id, Config.access_key_id)
32
+ @secret_access_key = options.fetch(:secret_access_key, Config.secret_access_key)
33
+ @proxy = options.fetch(:proxy, Config.proxy)
34
+ @timeout = options.fetch(:timeout, Config.timeout)
35
+ @use_ssl = options.fetch(:use_ssl, Config.use_ssl)
36
+ @chunk_size = options.fetch(:chunk_size, Config.chunk_size)
37
+ @debug = options.fetch(:debug, Config.debug)
34
38
  end
35
39
 
36
40
  # Makes request with given HTTP method, sets missing parameters,
@@ -43,7 +47,7 @@ module Sndacs
43
47
  #
44
48
  # ==== Options:
45
49
  # * <tt>:host</tt> - Hostname to connecto to, defaults
46
- # to <tt>s3.amazonaws.com</tt>
50
+ # to <tt>storage.grandcloud.cn</tt>
47
51
  # * <tt>:path</tt> - path to send request to (REQUIRED)
48
52
  # * <tt>:body</tt> - Request body, only meaningful for
49
53
  # <tt>:put</tt> request
@@ -55,7 +59,7 @@ module Sndacs
55
59
  # ==== Returns
56
60
  # Net::HTTPResponse object -- response from the server
57
61
  def request(method, options)
58
- host = options.fetch(:host, HOST)
62
+ host = options.fetch(:host, Config.host)
59
63
  path = options.fetch(:path)
60
64
  body = options.fetch(:body, nil)
61
65
  params = options.fetch(:params, {})
@@ -67,7 +71,10 @@ module Sndacs
67
71
 
68
72
  if params
69
73
  params = params.is_a?(String) ? params : self.class.parse_params(params)
70
- path << "?#{params}"
74
+
75
+ if params != ''
76
+ path << "?#{params}"
77
+ end
71
78
  end
72
79
 
73
80
  request = Request.new(@chunk_size, method.to_s.upcase, !!body, method.to_s.upcase != "HEAD", path)
@@ -83,6 +90,7 @@ module Sndacs
83
90
  else
84
91
  request.body = body
85
92
  end
93
+
86
94
  request.content_length = body.respond_to?(:lstat) ? body.stat.size : body.size
87
95
  end
88
96
 
@@ -105,6 +113,7 @@ module Sndacs
105
113
  params.each do |key, value|
106
114
  if interesting_keys.include?(key)
107
115
  parsed_key = key.to_s.gsub("_", "-")
116
+
108
117
  case value
109
118
  when nil
110
119
  result << parsed_key
@@ -113,6 +122,7 @@ module Sndacs
113
122
  end
114
123
  end
115
124
  end
125
+
116
126
  result.join("&")
117
127
  end
118
128
 
@@ -144,18 +154,21 @@ module Sndacs
144
154
  if interesting_keys.include?(key)
145
155
  parsed_key = key.to_s.gsub("_", "-")
146
156
  parsed_value = value
157
+
147
158
  case value
148
159
  when Range
149
160
  parsed_value = "bytes=#{value.first}-#{value.last}"
150
161
  end
162
+
151
163
  parsed_headers[parsed_key] = parsed_value
152
164
  end
153
165
  end
154
166
  end
167
+
155
168
  parsed_headers
156
169
  end
157
170
 
158
- private
171
+ private
159
172
 
160
173
  def port
161
174
  use_ssl ? 443 : 80
@@ -171,6 +184,7 @@ module Sndacs
171
184
  http.use_ssl = @use_ssl
172
185
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
173
186
  http.read_timeout = @timeout if @timeout
187
+
174
188
  http
175
189
  end
176
190
 
@@ -195,9 +209,17 @@ module Sndacs
195
209
  http.request(request)
196
210
  end
197
211
 
198
- if response.code.to_i == 307
212
+ case response.code.to_i
213
+ when 301
214
+ if response.body
215
+ doc = Document.new response.body
216
+
217
+ send_request(doc.elements["Error"].elements["Endpoint"].text, request, true)
218
+ end
219
+ when 307
199
220
  if response.body
200
221
  doc = Document.new response.body
222
+
201
223
  send_request(doc.elements["Error"].elements["Endpoint"].text, request, true)
202
224
  end
203
225
  else
@@ -214,12 +236,15 @@ module Sndacs
214
236
  raise Error::ResponseError.new(nil, response)
215
237
  else
216
238
  code, message = parse_error(response.body)
239
+
217
240
  raise Error::ResponseError.exception(code).new(message, response)
218
241
  end
219
242
  else
220
243
  raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
221
244
  end
245
+
222
246
  response
223
247
  end
224
248
  end
249
+
225
250
  end
@@ -1,4 +1,5 @@
1
1
  module Sndacs
2
+
2
3
  module Error
3
4
 
4
5
  # All responses with a code between 300 and 599 that contain an
@@ -12,7 +13,7 @@ module Sndacs
12
13
  class ResponseError < StandardError
13
14
  attr_reader :response
14
15
 
15
- # Creates new S3::ResponseError.
16
+ # Creates new Sndacs::ResponseError.
16
17
  #
17
18
  # ==== Parameters
18
19
  # * <tt>message</tt> - what went wrong
@@ -23,7 +24,7 @@ module Sndacs
23
24
  end
24
25
 
25
26
  # Factory for all other Exception classes in module, each for
26
- # every error response available from AmazonAWS
27
+ # every error response available from SNDA Cloud Storage
27
28
  #
28
29
  # ==== Parameters
29
30
  # * <tt>code</tt> - Code name of exception
@@ -32,7 +33,7 @@ module Sndacs
32
33
  # Descendant of ResponseError suitable for that exception code
33
34
  # or ResponseError class if no class found
34
35
  def self.exception(code)
35
- S3::Error.const_get(code)
36
+ Sndacs::Error.const_get(code)
36
37
  rescue NameError
37
38
  ResponseError
38
39
  end
@@ -52,6 +53,7 @@ module Sndacs
52
53
  class EntityTooSmall < ResponseError; end
53
54
  class EntityTooLarge < ResponseError; end
54
55
  class ExpiredToken < ResponseError; end
56
+ class ForbiddenBucket < ResponseError; end
55
57
  class IncompleteBody < ResponseError; end
56
58
  class IncorrectNumberOfFilesInPostRequestPOST < ResponseError; end
57
59
  class InlineDataTooLarge < ResponseError; end
@@ -107,4 +109,5 @@ module Sndacs
107
109
  class UnresolvableGrantByEmailAddress < ResponseError; end
108
110
  class UserKeyMustBeSpecified < ResponseError; end
109
111
  end
112
+
110
113
  end