fog 0.3.1 → 0.3.2

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.
Files changed (49) hide show
  1. data/Gemfile.lock +1 -1
  2. data/README.rdoc +1 -0
  3. data/fog.gemspec +44 -1
  4. data/lib/fog.rb +2 -1
  5. data/lib/fog/bin.rb +2 -0
  6. data/lib/fog/google.rb +22 -0
  7. data/lib/fog/google/bin.rb +20 -0
  8. data/lib/fog/google/models/storage/directories.rb +43 -0
  9. data/lib/fog/google/models/storage/directory.rb +48 -0
  10. data/lib/fog/google/models/storage/file.rb +87 -0
  11. data/lib/fog/google/models/storage/files.rb +94 -0
  12. data/lib/fog/google/parsers/storage/access_control_list.rb +46 -0
  13. data/lib/fog/google/parsers/storage/copy_object.rb +22 -0
  14. data/lib/fog/google/parsers/storage/get_bucket.rb +46 -0
  15. data/lib/fog/google/parsers/storage/get_bucket_logging.rb +40 -0
  16. data/lib/fog/google/parsers/storage/get_bucket_object_versions.rb +88 -0
  17. data/lib/fog/google/parsers/storage/get_bucket_versioning.rb +24 -0
  18. data/lib/fog/google/parsers/storage/get_request_payment.rb +20 -0
  19. data/lib/fog/google/parsers/storage/get_service.rb +32 -0
  20. data/lib/fog/google/requests/storage/copy_object.rb +72 -0
  21. data/lib/fog/google/requests/storage/delete_bucket.rb +46 -0
  22. data/lib/fog/google/requests/storage/delete_object.rb +50 -0
  23. data/lib/fog/google/requests/storage/get_bucket.rb +110 -0
  24. data/lib/fog/google/requests/storage/get_bucket_acl.rb +55 -0
  25. data/lib/fog/google/requests/storage/get_object.rb +104 -0
  26. data/lib/fog/google/requests/storage/get_object_acl.rb +66 -0
  27. data/lib/fog/google/requests/storage/get_object_torrent.rb +55 -0
  28. data/lib/fog/google/requests/storage/get_object_url.rb +54 -0
  29. data/lib/fog/google/requests/storage/get_service.rb +53 -0
  30. data/lib/fog/google/requests/storage/head_object.rb +64 -0
  31. data/lib/fog/google/requests/storage/put_bucket.rb +68 -0
  32. data/lib/fog/google/requests/storage/put_bucket_acl.rb +80 -0
  33. data/lib/fog/google/requests/storage/put_object.rb +71 -0
  34. data/lib/fog/google/requests/storage/put_object_url.rb +54 -0
  35. data/lib/fog/google/storage.rb +192 -0
  36. data/spec/google/models/storage/directories_spec.rb +49 -0
  37. data/spec/google/models/storage/directory_spec.rb +83 -0
  38. data/spec/google/models/storage/file_spec.rb +121 -0
  39. data/spec/google/models/storage/files_spec.rb +141 -0
  40. data/spec/google/requests/storage/copy_object_spec.rb +61 -0
  41. data/spec/google/requests/storage/delete_bucket_spec.rb +35 -0
  42. data/spec/google/requests/storage/delete_object_spec.rb +38 -0
  43. data/spec/google/requests/storage/get_bucket_spec.rb +110 -0
  44. data/spec/google/requests/storage/get_object_spec.rb +58 -0
  45. data/spec/google/requests/storage/get_service_spec.rb +32 -0
  46. data/spec/google/requests/storage/head_object_spec.rb +26 -0
  47. data/spec/google/requests/storage/put_bucket_spec.rb +21 -0
  48. data/spec/google/requests/storage/put_object_spec.rb +43 -0
  49. metadata +45 -2
@@ -0,0 +1,53 @@
1
+ module Fog
2
+ module Google
3
+ class Storage
4
+ class Real
5
+
6
+ require 'fog/google/parsers/storage/get_service'
7
+
8
+ # List information about Google Storage buckets for authorized user
9
+ #
10
+ # ==== Returns
11
+ # * response<~Excon::Response>:
12
+ # * body<~Hash>:
13
+ # * 'Buckets'<~Hash>:
14
+ # * 'Name'<~String> - Name of bucket
15
+ # * 'CreationTime'<~Time> - Timestamp of bucket creation
16
+ # * 'Owner'<~Hash>:
17
+ # * 'DisplayName'<~String> - Display name of bucket owner
18
+ # * 'ID'<~String> - Id of bucket owner
19
+ def get_service
20
+ request({
21
+ :expects => 200,
22
+ :headers => {},
23
+ :host => @host,
24
+ :idempotent => true,
25
+ :method => 'GET',
26
+ :parser => Fog::Parsers::Google::Storage::GetService.new,
27
+ :url => @host
28
+ })
29
+ end
30
+
31
+ end
32
+
33
+ class Mock
34
+
35
+ def get_service
36
+ response = Excon::Response.new
37
+ response.headers['Status'] = 200
38
+ buckets = @data[:buckets].values.map do |bucket|
39
+ bucket.reject do |key, value|
40
+ !['CreationDate', 'Name'].include?(key)
41
+ end
42
+ end
43
+ response.body = {
44
+ 'Buckets' => buckets,
45
+ 'Owner' => { 'DisplayName' => 'owner', 'ID' => 'some_id'}
46
+ }
47
+ response
48
+ end
49
+
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,64 @@
1
+ module Fog
2
+ module Google
3
+ class Storage
4
+ class Real
5
+
6
+ # Get headers for an object from Google Storage
7
+ #
8
+ # ==== Parameters
9
+ # * bucket_name<~String> - Name of bucket to read from
10
+ # * object_name<~String> - Name of object to read
11
+ # * options<~Hash>:
12
+ # * 'If-Match'<~String> - Returns object only if its etag matches this value, otherwise returns 412 (Precondition Failed).
13
+ # * 'If-Modified-Since'<~Time> - Returns object only if it has been modified since this time, otherwise returns 304 (Not Modified).
14
+ # * 'If-None-Match'<~String> - Returns object only if its etag differs from this value, otherwise returns 304 (Not Modified)
15
+ # * 'If-Unmodified-Since'<~Time> - Returns object only if it has not been modified since this time, otherwise returns 412 (Precodition Failed).
16
+ # * 'Range'<~String> - Range of object to download
17
+ # * 'versionId'<~String> - specify a particular version to retrieve
18
+ #
19
+ # ==== Returns
20
+ # * response<~Excon::Response>:
21
+ # * body<~String> - Contents of object
22
+ # * headers<~Hash>:
23
+ # * 'Content-Length'<~String> - Size of object contents
24
+ # * 'Content-Type'<~String> - MIME type of object
25
+ # * 'ETag'<~String> - Etag of object
26
+ # * 'Last-Modified'<~String> - Last modified timestamp for object
27
+ def head_object(bucket_name, object_name, options={})
28
+ unless bucket_name
29
+ raise ArgumentError.new('bucket_name is required')
30
+ end
31
+ unless object_name
32
+ raise ArgumentError.new('object_name is required')
33
+ end
34
+ if version_id = options.delete('versionId')
35
+ query = {'versionId' => version_id}
36
+ end
37
+ headers = {}
38
+ headers['If-Modified-Since'] = options['If-Modified-Since'].utc.strftime("%a, %d %b %Y %H:%M:%S +0000") if options['If-Modified-Since']
39
+ headers['If-Unmodified-Since'] = options['If-Unmodified-Since'].utc.strftime("%a, %d %b %Y %H:%M:%S +0000") if options['If-Modified-Since']
40
+ headers.merge!(options)
41
+ request({
42
+ :expects => 200,
43
+ :headers => headers,
44
+ :host => "#{bucket_name}.#{@host}",
45
+ :method => 'HEAD',
46
+ :path => CGI.escape(object_name),
47
+ :query => query
48
+ })
49
+ end
50
+
51
+ end
52
+
53
+ class Mock
54
+
55
+ def head_object(bucket_name, object_name, options = {})
56
+ response = get_object(bucket_name, object_name, options)
57
+ response.body = nil
58
+ response
59
+ end
60
+
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,68 @@
1
+ module Fog
2
+ module Google
3
+ class Storage
4
+ class Real
5
+
6
+ # Create an Google Storage bucket
7
+ #
8
+ # ==== Parameters
9
+ # * bucket_name<~String> - name of bucket to create
10
+ # * options<~Hash> - config arguments for bucket. Defaults to {}.
11
+ # * :location_constraint<~Symbol> - sets the location for the bucket
12
+ #
13
+ # ==== Returns
14
+ # * response<~Excon::Response>:
15
+ # * status<~Integer> - 200
16
+ def put_bucket(bucket_name, options = {})
17
+ if options['LocationConstraint']
18
+ data =
19
+ <<-DATA
20
+ <CreateBucketConfiguration>
21
+ <LocationConstraint>#{options['LocationConstraint']}</LocationConstraint>
22
+ </CreateBucketConfiguration>
23
+ DATA
24
+ else
25
+ data = nil
26
+ end
27
+ request({
28
+ :expects => 200,
29
+ :body => data,
30
+ :headers => {},
31
+ :idempotent => true,
32
+ :host => "#{bucket_name}.#{@host}",
33
+ :method => 'PUT'
34
+ })
35
+ end
36
+
37
+ end
38
+
39
+ class Mock
40
+
41
+ def put_bucket(bucket_name, options = {})
42
+ response = Excon::Response.new
43
+ response.status = 200
44
+ bucket = {
45
+ :objects => {},
46
+ 'Name' => bucket_name,
47
+ 'CreationDate' => Time.now,
48
+ 'Owner' => { 'DisplayName' => 'owner', 'ID' => 'some_id'},
49
+ 'Payer' => 'BucketOwner'
50
+ }
51
+ if options['LocationConstraint']
52
+ bucket['LocationConstraint'] = options['LocationConstraint']
53
+ else
54
+ bucket['LocationConstraint'] = ''
55
+ end
56
+ if @data[:buckets][bucket_name].nil?
57
+ @data[:buckets][bucket_name] = bucket
58
+ else
59
+ response.status = 409
60
+ raise(Excon::Errors.status_error({:expects => 200}, response))
61
+ end
62
+ response
63
+ end
64
+
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,80 @@
1
+ module Fog
2
+ module Google
3
+ class Storage
4
+ class Real
5
+
6
+ # Change access control list for an Google Storage bucket
7
+ #
8
+ # ==== Parameters
9
+ # * bucket_name<~String> - name of bucket to modify
10
+ # * acl<~Hash>:
11
+ # * Owner<~Hash>:
12
+ # * ID<~String>: id of owner
13
+ # * DisplayName<~String>: display name of owner
14
+ # * AccessControlList<~Array>:
15
+ # * Grantee<~Hash>:
16
+ # * 'DisplayName'<~String> - Display name of grantee
17
+ # * 'ID'<~String> - Id of grantee
18
+ # or
19
+ # * 'EmailAddress'<~String> - Email address of grantee
20
+ # or
21
+ # * 'URI'<~String> - URI of group to grant access for
22
+ # * Permission<~String> - Permission, in [FULL_CONTROL, WRITE, WRITE_ACP, READ, READ_ACP]
23
+ def put_bucket_acl(bucket_name, acl)
24
+ data =
25
+ <<-DATA
26
+ <AccessControlPolicy>
27
+ <Owner>
28
+ <ID>#{acl['Owner']['ID']}</ID>
29
+ <DisplayName>#{acl['Owner']['DisplayName']}</DisplayName>
30
+ </Owner>
31
+ <AccessControlList>
32
+ DATA
33
+
34
+ acl['AccessControlList'].each do |grant|
35
+ data << " <Grant>"
36
+ type = case grant['Grantee'].keys.sort
37
+ when ['DisplayName', 'ID']
38
+ 'CanonicalUser'
39
+ when ['EmailAddress']
40
+ 'AmazonCustomerByEmail'
41
+ when ['URI']
42
+ 'Group'
43
+ end
44
+ data << " <Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"#{type}\">"
45
+ for key, value in grant['Grantee']
46
+ data << " <#{key}>#{value}</#{key}>"
47
+ end
48
+ data << " </Grantee>"
49
+ data << " <Permission>#{grant['Permission']}</Permission>"
50
+ data << " </Grant>"
51
+ end
52
+
53
+ data <<
54
+ <<-DATA
55
+ </AccessControlList>
56
+ </AccessControlPolicy>
57
+ DATA
58
+
59
+ request({
60
+ :body => data,
61
+ :expects => 200,
62
+ :headers => {},
63
+ :host => "#{bucket_name}.#{@host}",
64
+ :method => 'PUT',
65
+ :query => {'acl' => nil}
66
+ })
67
+ end
68
+
69
+ end
70
+
71
+ class Mock
72
+
73
+ def put_bucket_acl(bucket_name, acl)
74
+ Fog::Mock.not_implemented
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,71 @@
1
+ module Fog
2
+ module Google
3
+ class Storage
4
+ class Real
5
+
6
+ # Create an object in an Google Storage bucket
7
+ #
8
+ # ==== Parameters
9
+ # * bucket_name<~String> - Name of bucket to create object in
10
+ # * object_name<~String> - Name of object to create
11
+ # * data<~File> - File or String to create object from
12
+ # * options<~Hash>:
13
+ # * 'Cache-Control'<~String> - Caching behaviour
14
+ # * 'Content-Disposition'<~String> - Presentational information for the object
15
+ # * 'Content-Encoding'<~String> - Encoding of object data
16
+ # * 'Content-Length'<~String> - Size of object in bytes (defaults to object.read.length)
17
+ # * 'Content-MD5'<~String> - Base64 encoded 128-bit MD5 digest of message (defaults to Base64 encoded MD5 of object.read)
18
+ # * 'Content-Type'<~String> - Standard MIME type describing contents (defaults to MIME::Types.of.first)
19
+ # * 'x-goog-acl'<~String> - Permissions, must be in ['private', 'public-read', 'public-read-write', 'authenticated-read']
20
+ # * "x-goog-meta-#{name}" - Headers to be returned with object, note total size of request without body must be less than 8 KB.
21
+ #
22
+ # ==== Returns
23
+ # * response<~Excon::Response>:
24
+ # * headers<~Hash>:
25
+ # * 'ETag'<~String> - etag of new object
26
+ def put_object(bucket_name, object_name, data, options = {})
27
+ data = parse_data(data)
28
+ headers = data[:headers].merge!(options)
29
+ request({
30
+ :body => data[:body],
31
+ :expects => 200,
32
+ :headers => headers,
33
+ :host => "#{bucket_name}.#{@host}",
34
+ :idempotent => true,
35
+ :method => 'PUT',
36
+ :path => CGI.escape(object_name)
37
+ })
38
+ end
39
+
40
+ end
41
+
42
+ class Mock
43
+
44
+ def put_object(bucket_name, object_name, data, options = {})
45
+ data = parse_data(data)
46
+ unless data[:body].is_a?(String)
47
+ data[:body] = data[:body].read
48
+ end
49
+ response = Excon::Response.new
50
+ if (bucket = @data[:buckets][bucket_name])
51
+ response.status = 200
52
+ bucket[:objects][object_name] = {
53
+ :body => data[:body],
54
+ 'ETag' => Fog::Google::Mock.etag,
55
+ 'Key' => object_name,
56
+ 'LastModified' => Time.now.utc.strftime("%a, %d %b %Y %H:%M:%S +0000"),
57
+ 'Size' => data[:headers]['Content-Length'],
58
+ 'StorageClass' => 'STANDARD'
59
+ }
60
+ bucket[:objects][object_name]['Content-Type'] = data[:headers]['Content-Type']
61
+ else
62
+ response.status = 404
63
+ raise(Excon::Errors.status_error({:expects => 200}, response))
64
+ end
65
+ response
66
+ end
67
+
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,54 @@
1
+ module Fog
2
+ module Google
3
+ class Storage
4
+ class Real
5
+
6
+ # Get an expiring object url from Google Storage for putting an object
7
+ #
8
+ # ==== Parameters
9
+ # * bucket_name<~String> - Name of bucket containing object
10
+ # * object_name<~String> - Name of object to get expiring url for
11
+ # * expires<~Time> - An expiry time for this url
12
+ #
13
+ # ==== Returns
14
+ # * response<~Excon::Response>:
15
+ # * body<~String> - url for object
16
+ #
17
+ def put_object_url(bucket_name, object_name, expires)
18
+ unless bucket_name
19
+ raise ArgumentError.new('bucket_name is required')
20
+ end
21
+ unless object_name
22
+ raise ArgumentError.new('object_name is required')
23
+ end
24
+ url({
25
+ :headers => {},
26
+ :host => "#{bucket_name}.#{@host}",
27
+ :method => 'PUT',
28
+ :path => CGI.escape(object_name)
29
+ }, expires)
30
+ end
31
+
32
+ end
33
+
34
+ class Mock
35
+
36
+ def put_object_url(bucket_name, object_name, expires)
37
+ unless bucket_name
38
+ raise ArgumentError.new('bucket_name is required')
39
+ end
40
+ unless object_name
41
+ raise ArgumentError.new('object_name is required')
42
+ end
43
+ url({
44
+ :headers => {},
45
+ :host => "#{bucket_name}.#{@host}",
46
+ :method => 'PUT',
47
+ :path => CGI.escape(object_name)
48
+ }, expires)
49
+ end
50
+
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,192 @@
1
+ module Fog
2
+ module Google
3
+ class Storage < Fog::Service
4
+
5
+ requires :google_storage_access_key_id, :google_storage_secret_access_key
6
+
7
+ model_path 'fog/google/models/storage'
8
+ collection :directories
9
+ model :directory
10
+ collection :files
11
+ model :file
12
+
13
+ request_path 'fog/google/requests/storage'
14
+ request :copy_object
15
+ request :delete_bucket
16
+ request :delete_object
17
+ request :get_bucket
18
+ request :get_bucket_acl
19
+ request :get_object
20
+ request :get_object_acl
21
+ request :get_object_torrent
22
+ request :get_object_url
23
+ request :get_service
24
+ request :head_object
25
+ request :put_bucket
26
+ request :put_bucket_acl
27
+ request :put_object
28
+ request :put_object_url
29
+
30
+ module Utils
31
+
32
+ def parse_data(data)
33
+ metadata = {
34
+ :body => nil,
35
+ :headers => {}
36
+ }
37
+
38
+ if data.is_a?(String)
39
+ metadata[:body] = data
40
+ metadata[:headers]['Content-Length'] = metadata[:body].size.to_s
41
+ else
42
+ filename = ::File.basename(data.path)
43
+ unless (mime_types = MIME::Types.of(filename)).empty?
44
+ metadata[:headers]['Content-Type'] = mime_types.first.content_type
45
+ end
46
+ metadata[:body] = data
47
+ metadata[:headers]['Content-Length'] = ::File.size(data.path).to_s
48
+ end
49
+ # metadata[:headers]['Content-MD5'] = Base64.encode64(Digest::MD5.digest(metadata[:body])).strip
50
+ metadata
51
+ end
52
+
53
+ def url(params, expires)
54
+ params[:headers]['Date'] = expires.to_i
55
+ query = [params[:query]].compact
56
+ query << "GoogleAccessKeyId=#{@google_storage_access_key_id}"
57
+ query << "Signature=#{CGI.escape(signature(params))}"
58
+ query << "Expires=#{params[:headers]['Date']}"
59
+ "http://#{params[:host]}/#{params[:path]}?#{query.join('&')}"
60
+ end
61
+
62
+ end
63
+
64
+ class Mock
65
+ include Utils
66
+
67
+ def self.data
68
+ @data ||= Hash.new do |hash, key|
69
+ hash[key] = {
70
+ :buckets => {}
71
+ }
72
+ end
73
+ end
74
+
75
+ def self.reset_data(keys=data.keys)
76
+ for key in [*keys]
77
+ data.delete(key)
78
+ end
79
+ end
80
+
81
+ def initialize(options={})
82
+ @google_storage_access_key_id = options[:google_storage_access_key_id]
83
+ @data = self.class.data[@google_storage_access_key_id]
84
+ end
85
+
86
+ def signature(params)
87
+ "foo"
88
+ end
89
+ end
90
+
91
+ class Real
92
+ include Utils
93
+ extend Fog::Deprecation
94
+ deprecate(:reset, :reload)
95
+
96
+ # Initialize connection to Google Storage
97
+ #
98
+ # ==== Notes
99
+ # options parameter must include values for :google_storage_access_key_id and
100
+ # :google_storage_secret_access_key in order to create a connection
101
+ #
102
+ # ==== Examples
103
+ # google_storage = Storage.new(
104
+ # :google_storage_access_key_id => your_google_storage_access_key_id,
105
+ # :google_storage_secret_access_key => your_google_storage_secret_access_key
106
+ # )
107
+ #
108
+ # ==== Parameters
109
+ # * options<~Hash> - config arguments for connection. Defaults to {}.
110
+ #
111
+ # ==== Returns
112
+ # * Storage object with connection to google.
113
+ def initialize(options={})
114
+ @google_storage_access_key_id = options[:google_storage_access_key_id]
115
+ @google_storage_secret_access_key = options[:google_storage_secret_access_key]
116
+ @hmac = Fog::HMAC.new('sha1', @google_storage_secret_access_key)
117
+ @host = options[:host] || 'commondatastorage.googleapis.com'
118
+ @port = options[:port] || 443
119
+ @scheme = options[:scheme] || 'https'
120
+ @connection = Fog::Connection.new("#{@scheme}://#{@host}:#{@port}", options[:persistent] || true)
121
+ end
122
+
123
+ def reload
124
+ @connection.reset
125
+ end
126
+
127
+ private
128
+
129
+ def request(params, &block)
130
+ params[:headers]['Date'] = Time.now.utc.strftime("%a, %d %b %Y %H:%M:%S +0000")
131
+ params[:headers]['Authorization'] = "GOOG1 #{@google_storage_access_key_id}:#{signature(params)}"
132
+
133
+ response = @connection.request(params, &block)
134
+
135
+ response
136
+ end
137
+
138
+ def signature(params)
139
+ string_to_sign =
140
+ <<-DATA
141
+ #{params[:method]}
142
+ #{params[:headers]['Content-MD5']}
143
+ #{params[:headers]['Content-Type']}
144
+ #{params[:headers]['Date']}
145
+ DATA
146
+
147
+ google_headers, canonical_google_headers = {}, ''
148
+ for key, value in params[:headers]
149
+ if key[0..6] == 'x-goog-'
150
+ google_headers[key] = value
151
+ end
152
+ end
153
+
154
+ google_headers = google_headers.sort {|x, y| x[0] <=> y[0]}
155
+ for key, value in google_headers
156
+ canonical_google_headers << "#{key}:#{value}\n"
157
+ end
158
+ string_to_sign << "#{canonical_google_headers}"
159
+
160
+ subdomain = params[:host].split(".#{@host}").first
161
+ unless subdomain =~ /^(?:[a-z]|\d(?!\d{0,2}(?:\.\d{1,3}){3}$))(?:[a-z0-9]|\.(?![\.\-])|\-(?![\.])){1,61}[a-z0-9]$/
162
+ Formatador.display_line("[yellow][WARN] fog: the specified google storage bucket name(#{subdomain}) is not a valid dns name. See: http://code.google.com/apis/storage/docs/developer-guide.html#naming[/]")
163
+ params[:host] = params[:host].split("#{subdomain}.")[-1]
164
+ if params[:path]
165
+ params[:path] = "#{subdomain}/#{params[:path]}"
166
+ else
167
+ params[:path] = "#{subdomain}"
168
+ end
169
+ subdomain = nil
170
+ end
171
+
172
+ canonical_resource = "/"
173
+ unless subdomain.nil? || subdomain == @host
174
+ canonical_resource << "#{CGI.escape(subdomain).downcase}/"
175
+ end
176
+ canonical_resource << "#{params[:path]}"
177
+ canonical_resource << '?'
178
+ for key in (params[:query] || {}).keys
179
+ if ['acl', 'location', 'logging', 'requestPayment', 'torrent', 'versions', 'versioning'].include?(key)
180
+ canonical_resource << "#{key}&"
181
+ end
182
+ end
183
+ canonical_resource.chop!
184
+ string_to_sign << "#{canonical_resource}"
185
+
186
+ signed_string = @hmac.sign(string_to_sign)
187
+ signature = Base64.encode64(signed_string).chomp!
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end