sndacs 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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