sndacs 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,17 @@
1
+ *.swp
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ -f nested --color
2
+ --order rand
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,33 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sndacs (0.0.1)
5
+ proxies (~> 0.2.0)
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ diff-lcs (1.1.3)
11
+ metaclass (0.0.1)
12
+ mocha (0.10.5)
13
+ metaclass (~> 0.0.1)
14
+ proxies (0.2.1)
15
+ rspec (2.9.0)
16
+ rspec-core (~> 2.9.0)
17
+ rspec-expectations (~> 2.9.0)
18
+ rspec-mocks (~> 2.9.0)
19
+ rspec-core (2.9.0)
20
+ rspec-expectations (2.9.0)
21
+ diff-lcs (~> 1.1.3)
22
+ rspec-mocks (2.9.0)
23
+ test-unit (2.4.8)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ bundler (>= 1.0.0)
30
+ mocha
31
+ rspec (~> 2.0)
32
+ sndacs!
33
+ test-unit (>= 2.0)
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2009 Jakub Kuźma, Mirosław Boruta
2
+ Copyright (c) 2012 LI Daobing <lidaobing@snda.com>
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,80 @@
1
+ = sndacs
2
+
3
+ sndacs library provides access to {SNDA Cloud Storage}[http://www.grandcloud.cn/product/ecs].
4
+
5
+ bases on the s3 gem: https://github.com/qoobaa/s3
6
+
7
+ == Installation
8
+
9
+ gem install sndacs
10
+
11
+ == Usage
12
+
13
+ === Initialize the service
14
+
15
+ require "sndacs"
16
+ service = Sndacs::Service.new(:access_key_id => "...",
17
+ :secret_access_key => "...")
18
+ #=> #<Sndacs::Service:...>
19
+
20
+ === List buckets
21
+
22
+ service.buckets
23
+ #=> [#<Sndacs::Bucket:first-bucket>,
24
+ # #<Sndacs::Bucket:second-bucket>]
25
+
26
+ === Find bucket
27
+
28
+ first_bucket = service.buckets.find("first-bucket")
29
+ #=> #<Sndacs::Bucket:first-bucket>
30
+
31
+ === List objects in a bucket
32
+
33
+ first_bucket.objects
34
+ #=> [#<Sndacs::Object:/first-bucket/lenna.png>,
35
+ # #<Sndacs::Object:/first-bucket/lenna_mini.png>]
36
+
37
+ === Find object in a bucket
38
+
39
+ object = first_bucket.objects.find("lenna.png")
40
+ #=> #<Sndacs::Object:/first-bucket/lenna.png>
41
+
42
+ === Access object metadata (cached from find)
43
+
44
+ object.content_type
45
+ #=> "image/png"
46
+
47
+ === Access object content (downloads the object)
48
+
49
+ object.content
50
+ #=> "\x89PNG\r\n\x1A\n\x00\x00\x00\rIHDR\x00..."
51
+
52
+ === Delete an object
53
+
54
+ object.destroy
55
+ #=> true
56
+
57
+ === Create an object
58
+
59
+ new_object = bucket.objects.build("bender.png")
60
+ #=> #<Sndacs::Object:/synergy-staging/bender.png>
61
+
62
+ new_object.content = open("bender.png")
63
+
64
+ new_object.save
65
+ #=> true
66
+
67
+ Please note that new objects are created with "public-read" ACL by
68
+ default.
69
+
70
+ == See also
71
+
72
+ * rubygems[http://rubygems.org/gems/s3]
73
+ * repository[http://github.com/qoobaa/s3]
74
+ * {issue tracker}[http://github.com/qoobaa/s3/issues]
75
+ * documentation[http://rubydoc.info/github/qoobaa/s3/master/frames]
76
+
77
+ == Copyright
78
+
79
+ Copyright (c) 2012 LI Daobing. See LICENSE[http://github.com/grandcloud/sndacs-ruby/raw/master/LICENSE] for details.
80
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler"
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new :spec
6
+ task :default => :spec
@@ -0,0 +1,179 @@
1
+ module Sndacs
2
+ class Bucket
3
+ include Parser
4
+ include Proxies
5
+ extend Forwardable
6
+
7
+ attr_reader :name, :service
8
+
9
+ def_instance_delegators :service, :service_request
10
+ private_class_method :new
11
+
12
+ # Retrieves the bucket information from the server. Raises an
13
+ # S3::Error exception if the bucket doesn't exist or you don't
14
+ # have access to it, etc.
15
+ def retrieve
16
+ bucket_headers
17
+ self
18
+ end
19
+
20
+ # Returns location of the bucket, e.g. "EU"
21
+ def location(reload = false)
22
+ return @location if defined?(@location) and not reload
23
+ @location = location_constraint
24
+ end
25
+
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
32
+
33
+ # Similar to retrieve, but catches S3::Error::NoSuchBucket
34
+ # exceptions and returns false instead.
35
+ def exists?
36
+ retrieve
37
+ true
38
+ rescue Error::NoSuchBucket
39
+ false
40
+ end
41
+
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)
45
+ def destroy(force = false)
46
+ delete_bucket
47
+ true
48
+ rescue Error::BucketNotEmpty
49
+ if force
50
+ objects.destroy_all
51
+ retry
52
+ else
53
+ raise
54
+ end
55
+ end
56
+
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)
67
+ true
68
+ end
69
+
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>)
73
+ def vhost?
74
+ #"#@name.#{HOST}" =~ /\A#{URI::REGEXP::PATTERN::HOSTNAME}\Z/
75
+ false
76
+ end
77
+
78
+ # Returns host name of the bucket according (see #vhost? method)
79
+ def host
80
+ vhost? ? "#@name.#{HOST}" : "#{HOST}"
81
+ end
82
+
83
+ # Returns path prefix for non +VHOST+ bucket. Path prefix is used
84
+ # instead of +VHOST+ name, e.g. "bucket_name/"
85
+ def path_prefix
86
+ vhost? ? "" : "#@name/"
87
+ end
88
+
89
+ # Returns the objects in the bucket and caches the result
90
+ def objects
91
+ Proxy.new(lambda { list_bucket }, :owner => self, :extend => ObjectsExtension)
92
+ end
93
+
94
+ # Returns the object with the given key. Does not check whether the
95
+ # object exists. But also does not issue any HTTP requests, so it's
96
+ # much faster than objects.find
97
+ def object(key)
98
+ Object.send(:new, self, :key => key)
99
+ end
100
+
101
+ def inspect #:nodoc:
102
+ "#<#{self.class}:#{name}>"
103
+ end
104
+
105
+ private
106
+
107
+ attr_writer :service
108
+
109
+ def location_constraint
110
+ response = bucket_request(:get, :params => {:location => nil})
111
+ parse_location_constraint(response.body)
112
+ end
113
+
114
+ def list_bucket(options = {})
115
+ response = bucket_request(:get, :params => options)
116
+ max_keys = options[:max_keys]
117
+ objects_attributes = parse_list_bucket_result(response.body)
118
+
119
+ # If there are more than 1000 objects S3 truncates listing and
120
+ # we need to request another listing for the remaining objects.
121
+ while parse_is_truncated(response.body)
122
+ next_request_options = {:marker => objects_attributes.last[:key]}
123
+
124
+ if max_keys
125
+ break if objects_attributes.length >= max_keys
126
+ next_request_options[:max_keys] = max_keys - objects_attributes.length
127
+ end
128
+
129
+ response = bucket_request(:get, :params => options.merge(next_request_options))
130
+ objects_attributes += parse_list_bucket_result(response.body)
131
+ end
132
+
133
+ objects_attributes.map { |object_attributes| Object.send(:new, self, object_attributes) }
134
+ end
135
+
136
+ def bucket_headers(options = {})
137
+ response = bucket_request(:head, :params => options)
138
+ rescue Error::ResponseError => e
139
+ if e.response.code.to_i == 404
140
+ raise Error::ResponseError.exception("NoSuchBucket").new("The specified bucket does not exist.", nil)
141
+ else
142
+ raise e
143
+ end
144
+ end
145
+
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)
154
+ end
155
+
156
+ def delete_bucket
157
+ bucket_request(:delete)
158
+ end
159
+
160
+ def initialize(service, name) #:nodoc:
161
+ self.service = service
162
+ self.name = name
163
+ end
164
+
165
+ def name=(name)
166
+ raise ArgumentError.new("Invalid bucket name: #{name}") unless name_valid?(name)
167
+ @name = name
168
+ end
169
+
170
+ def bucket_request(method, options = {})
171
+ path = "#{path_prefix}#{options[:path]}"
172
+ service_request(method, options.merge(:host => host, :path => path))
173
+ end
174
+
175
+ def name_valid?(name)
176
+ name =~ /\A[a-z0-9][a-z0-9\._-]{2,254}\Z/i and name !~ /\A#{URI::REGEXP::PATTERN::IPV4ADDR}\Z/
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,26 @@
1
+ module Sndacs
2
+ module BucketsExtension
3
+ # Builds new bucket with given name
4
+ def build(name)
5
+ Bucket.send(:new, proxy_owner, name)
6
+ end
7
+
8
+ # Finds the bucket with given name
9
+ def find_first(name)
10
+ bucket = build(name)
11
+ bucket.retrieve
12
+ end
13
+ alias :find :find_first
14
+
15
+ # Finds all buckets in the service
16
+ def find_all
17
+ proxy_target
18
+ end
19
+
20
+ # Destroys all buckets in the service. Doesn't destroy non-empty
21
+ # buckets by default, pass true to force destroy (USE WITH CARE!).
22
+ def destroy_all(force = false)
23
+ proxy_target.each { |bucket| bucket.destroy(force) }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,225 @@
1
+ module Sndacs
2
+
3
+ # Class responsible for handling connections to amazon hosts
4
+ class Connection
5
+ include Parser
6
+
7
+ attr_accessor :access_key_id, :secret_access_key, :use_ssl, :timeout, :debug, :proxy
8
+ alias :use_ssl? :use_ssl
9
+
10
+ # Creates new connection object.
11
+ #
12
+ # ==== Options
13
+ # * <tt>:access_key_id</tt> - Access key id (REQUIRED)
14
+ # * <tt>:secret_access_key</tt> - Secret access key (REQUIRED)
15
+ # * <tt>:use_ssl</tt> - Use https or http protocol (false by
16
+ # default)
17
+ # * <tt>:debug</tt> - Display debug information on the STDOUT
18
+ # (false by default)
19
+ # * <tt>:timeout</tt> - Timeout to use by the Net::HTTP object
20
+ # (60 by default)
21
+ # * <tt>:proxy</tt> - Hash for Net::HTTP Proxy settings
22
+ # { :host => "proxy.mydomain.com", :port => "80, :user => "user_a", :password => "secret" }
23
+ # * <tt>:proxy</tt> - Hash for Net::HTTP Proxy settings
24
+ # * <tt>:chunk_size</tt> - Size of a chunk when streaming
25
+ # (1048576 (1 MiB) by default)
26
+ 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)
34
+ end
35
+
36
+ # Makes request with given HTTP method, sets missing parameters,
37
+ # adds signature to request header and returns response object
38
+ # (Net::HTTPResponse)
39
+ #
40
+ # ==== Parameters
41
+ # * <tt>method</tt> - HTTP Method symbol, can be <tt>:get</tt>,
42
+ # <tt>:put</tt>, <tt>:delete</tt>
43
+ #
44
+ # ==== Options:
45
+ # * <tt>:host</tt> - Hostname to connecto to, defaults
46
+ # to <tt>s3.amazonaws.com</tt>
47
+ # * <tt>:path</tt> - path to send request to (REQUIRED)
48
+ # * <tt>:body</tt> - Request body, only meaningful for
49
+ # <tt>:put</tt> request
50
+ # * <tt>:params</tt> - Parameters to add to query string for
51
+ # request, can be String or Hash
52
+ # * <tt>:headers</tt> - Hash of headers fields to add to request
53
+ # header
54
+ #
55
+ # ==== Returns
56
+ # Net::HTTPResponse object -- response from the server
57
+ def request(method, options)
58
+ host = options.fetch(:host, HOST)
59
+ path = options.fetch(:path)
60
+ body = options.fetch(:body, nil)
61
+ params = options.fetch(:params, {})
62
+ headers = options.fetch(:headers, {})
63
+
64
+ # Must be done before adding params
65
+ # Encodes all characters except forward-slash (/) and explicitly legal URL characters
66
+ path = URI.escape(path, /[^#{URI::REGEXP::PATTERN::UNRESERVED}\/]/)
67
+
68
+ if params
69
+ params = params.is_a?(String) ? params : self.class.parse_params(params)
70
+ path << "?#{params}"
71
+ end
72
+
73
+ request = Request.new(@chunk_size, method.to_s.upcase, !!body, method.to_s.upcase != "HEAD", path)
74
+
75
+ headers = self.class.parse_headers(headers)
76
+ headers.each do |key, value|
77
+ request[key] = value
78
+ end
79
+
80
+ if body
81
+ if body.respond_to?(:read)
82
+ request.body_stream = body
83
+ else
84
+ request.body = body
85
+ end
86
+ request.content_length = body.respond_to?(:lstat) ? body.stat.size : body.size
87
+ end
88
+
89
+ send_request(host, request)
90
+ end
91
+
92
+ # Helper function to parser parameters and create single string of
93
+ # params added to questy string
94
+ #
95
+ # ==== Parameters
96
+ # * <tt>params</tt> - Hash of parameters
97
+ #
98
+ # ==== Returns
99
+ # String -- containing all parameters joined in one params string,
100
+ # i.e. <tt>param1=val&param2&param3=0</tt>
101
+ def self.parse_params(params)
102
+ interesting_keys = [:max_keys, :prefix, :marker, :delimiter, :location]
103
+
104
+ result = []
105
+ params.each do |key, value|
106
+ if interesting_keys.include?(key)
107
+ parsed_key = key.to_s.gsub("_", "-")
108
+ case value
109
+ when nil
110
+ result << parsed_key
111
+ else
112
+ result << "#{parsed_key}=#{value}"
113
+ end
114
+ end
115
+ end
116
+ result.join("&")
117
+ end
118
+
119
+ # Helper function to change headers from symbols, to in correct
120
+ # form (i.e. with '-' instead of '_')
121
+ #
122
+ # ==== Parameters
123
+ # * <tt>headers</tt> - Hash of pairs <tt>headername => value</tt>,
124
+ # where value can be Range (for Range header) or any other value
125
+ # which can be translated to string
126
+ #
127
+ # ==== Returns
128
+ # Hash of headers translated from symbol to string, containing
129
+ # only interesting headers
130
+ def self.parse_headers(headers)
131
+ interesting_keys = [:content_type, :cache_control, :x_snda_acl, :x_snda_storage_class, :range,
132
+ :if_modified_since, :if_unmodified_since,
133
+ :if_match, :if_none_match,
134
+ :content_disposition, :content_encoding,
135
+ :x_snda_copy_source, :x_snda_metadata_directive,
136
+ :x_snda_copy_source_if_match,
137
+ :x_snda_copy_source_if_none_match,
138
+ :x_snda_copy_source_if_unmodified_since,
139
+ :x_snda_copy_source_if_modified_since]
140
+
141
+ parsed_headers = {}
142
+ if headers
143
+ headers.each do |key, value|
144
+ if interesting_keys.include?(key)
145
+ parsed_key = key.to_s.gsub("_", "-")
146
+ parsed_value = value
147
+ case value
148
+ when Range
149
+ parsed_value = "bytes=#{value.first}-#{value.last}"
150
+ end
151
+ parsed_headers[parsed_key] = parsed_value
152
+ end
153
+ end
154
+ end
155
+ parsed_headers
156
+ end
157
+
158
+ private
159
+
160
+ def port
161
+ use_ssl ? 443 : 80
162
+ end
163
+
164
+ def proxy_settings
165
+ @proxy.values_at(:host, :port, :user, :password) unless @proxy.nil? || @proxy.empty?
166
+ end
167
+
168
+ def http(host)
169
+ http = Net::HTTP.new(host, port, *proxy_settings)
170
+ http.set_debug_output(STDOUT) if @debug
171
+ http.use_ssl = @use_ssl
172
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
173
+ http.read_timeout = @timeout if @timeout
174
+ http
175
+ end
176
+
177
+ def send_request(host, request, skip_authorization = false)
178
+ response = http(host).start do |http|
179
+ host = http.address
180
+
181
+ request["Date"] ||= Time.now.httpdate
182
+
183
+ if request.body
184
+ request["Content-Type"] ||= "application/octet-stream"
185
+ request["Content-MD5"] = Base64.encode64(Digest::MD5.digest(request.body)).chomp unless request.body.empty?
186
+ end
187
+
188
+ unless skip_authorization
189
+ request["Authorization"] = Signature.generate(:host => host,
190
+ :request => request,
191
+ :access_key_id => access_key_id,
192
+ :secret_access_key => secret_access_key)
193
+ end
194
+
195
+ http.request(request)
196
+ end
197
+
198
+ if response.code.to_i == 307
199
+ if response.body
200
+ doc = Document.new response.body
201
+ send_request(doc.elements["Error"].elements["Endpoint"].text, request, true)
202
+ end
203
+ else
204
+ handle_response(response)
205
+ end
206
+ end
207
+
208
+ def handle_response(response)
209
+ case response.code.to_i
210
+ when 200...300
211
+ response
212
+ when 300...600
213
+ if response.body.nil? || response.body.empty?
214
+ raise Error::ResponseError.new(nil, response)
215
+ else
216
+ code, message = parse_error(response.body)
217
+ raise Error::ResponseError.exception(code).new(message, response)
218
+ end
219
+ else
220
+ raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
221
+ end
222
+ response
223
+ end
224
+ end
225
+ end