s3sync 1.2.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile DELETED
@@ -1,35 +0,0 @@
1
- require 'rubygems'
2
- require 'rake'
3
- require 'rake/clean'
4
- require 'rake/testtask'
5
- require 'rake/packagetask'
6
- require 'rake/gempackagetask'
7
- require 'rake/rdoctask'
8
- require File.join(File.dirname(__FILE__), 'lib', 'version')
9
-
10
- Gem::manage_gems
11
-
12
- readmes = ["README","README_s3cmd"]
13
-
14
- spec = Gem::Specification.new do |s|
15
- s.platform = Gem::Platform::RUBY
16
- s.name = "s3sync"
17
- s.version = S3sync::VERSION::STRING
18
- s.author = ""
19
- s.email = ""
20
- s.homepage = "http://s3sync.net/"
21
- s.rubyforge_project = "s3sync"
22
- s.summary = "rsync-like client for backing up to Amazons S3"
23
- s.files = Dir.glob("{bin,lib,docs}/**/*.rb") + ["Rakefile", "setup.rb", "CHANGELOG"] + readmes
24
- s.require_path = "lib"
25
- s.executables = ['s3sync','s3cmd']
26
- s.has_rdoc = true
27
- s.extra_rdoc_files = readmes
28
- end
29
- Rake::GemPackageTask.new(spec) do |pkg|
30
- pkg.need_zip = true
31
- pkg.need_tar = true
32
- end
33
- task :default => "pkg/#{spec.name}-#{spec.version}.gem" do
34
- puts "generated latest version"
35
- end
data/bin/s3cmd DELETED
@@ -1,245 +0,0 @@
1
- #! /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby
2
- # This software code is made available "AS IS" without warranties of any
3
- # kind. You may copy, display, modify and redistribute the software
4
- # code either by itself or as incorporated into your code; provided that
5
- # you do not remove any proprietary notices. Your use of this software
6
- # code is at your own risk and you waive any claim against the author
7
- # with respect to your use of this software code.
8
- # (c) 2007 s3sync.net
9
- #
10
-
11
- module S3sync
12
-
13
- # always look "here" for include files (thanks aktxyz)
14
- $LOAD_PATH << File.expand_path(File.dirname(__FILE__))
15
-
16
- require 's3try'
17
-
18
- $S3CMD_VERSION = '1.2.5'
19
-
20
- require 'getoptlong'
21
-
22
- # after other mods, so we don't overwrite yaml vals with defaults
23
- require 's3config'
24
- include S3Config
25
-
26
- def S3sync.s3cmdMain
27
- # ---------- OPTIONS PROCESSING ---------- #
28
-
29
- $S3syncOptions = Hash.new
30
- optionsParser = GetoptLong.new(
31
- [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
32
- [ '--ssl', '-s', GetoptLong::NO_ARGUMENT ],
33
- [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
34
- [ '--dryrun', '-n', GetoptLong::NO_ARGUMENT ],
35
- [ '--debug', '-d', GetoptLong::NO_ARGUMENT ],
36
- [ '--progress', GetoptLong::NO_ARGUMENT ],
37
- [ '--expires-in', GetoptLong::REQUIRED_ARGUMENT ]
38
- )
39
-
40
- def S3sync.s3cmdUsage(message = nil)
41
- $stderr.puts message if message
42
- name = $0.split('/').last
43
- $stderr.puts <<"ENDUSAGE"
44
- #{name} [options] <command> [arg(s)]\t\tversion #{$S3CMD_VERSION}
45
- --help -h --verbose -v --dryrun -n
46
- --ssl -s --debug -d --progress
47
- --expires-in=( <# of seconds> | [#d|#h|#m|#s] )
48
-
49
- Commands:
50
- #{name} listbuckets [headers]
51
- #{name} createbucket <bucket> [constraint (i.e. EU)]
52
- #{name} deletebucket <bucket> [headers]
53
- #{name} list <bucket>[:prefix] [max/page] [delimiter] [headers]
54
- #{name} location <bucket> [headers]
55
- #{name} delete <bucket>:key [headers]
56
- #{name} deleteall <bucket>[:prefix] [headers]
57
- #{name} get|put <bucket>:key <file> [headers]
58
- ENDUSAGE
59
- exit
60
- end #usage
61
-
62
- begin
63
- optionsParser.each {|opt, arg| $S3syncOptions[opt] = (arg || true)}
64
- rescue StandardError
65
- s3cmdUsage # the parser already printed an error message
66
- end
67
- s3cmdUsage if $S3syncOptions['--help']
68
- $S3syncOptions['--verbose'] = true if $S3syncOptions['--dryrun'] or $S3syncOptions['--debug'] or $S3syncOptions['--progress']
69
- $S3syncOptions['--ssl'] = true if $S3syncOptions['--ssl'] # change from "" to true to appease s3 port chooser
70
-
71
- if $S3syncOptions['--expires-in'] =~ /d|h|m|s/
72
- e = $S3syncOptions['--expires-in']
73
- days = (e =~ /(\d+)d/)? (/(\d+)d/.match(e))[1].to_i : 0
74
- hours = (e =~ /(\d+)h/)? (/(\d+)h/.match(e))[1].to_i : 0
75
- minutes = (e =~ /(\d+)m/)? (/(\d+)m/.match(e))[1].to_i : 0
76
- seconds = (e =~ /(\d+)s/)? (/(\d+)s/.match(e))[1].to_i : 0
77
- $S3syncOptions['--expires-in'] = seconds + 60 * ( minutes + 60 * ( hours + 24 * ( days ) ) )
78
- end
79
-
80
- # ---------- CONNECT ---------- #
81
- S3sync::s3trySetup
82
- # ---------- COMMAND PROCESSING ---------- #
83
-
84
- command, path, file = ARGV
85
-
86
- s3cmdUsage("You didn't set up your environment variables; see README.txt") if not($AWS_ACCESS_KEY_ID and $AWS_SECRET_ACCESS_KEY)
87
- s3cmdUsage("Need a command (etc)") if not command
88
-
89
- path = '' unless path
90
- path = path.dup # modifiable
91
- path += ':' unless path.match(':')
92
- bucket = (/^(.*?):/.match(path))[1]
93
- path.replace((/:(.*)$/.match(path))[1])
94
-
95
- case command
96
- when "delete"
97
- s3cmdUsage("Need a bucket") if bucket == ''
98
- s3cmdUsage("Need a key") if path == ''
99
- headers = hashPairs(ARGV[2...ARGV.length])
100
- $stderr.puts "delete #{bucket}:#{path} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
101
- S3try(:delete, bucket, path) unless $S3syncOptions['--dryrun']
102
- when "deleteall"
103
- s3cmdUsage("Need a bucket") if bucket == ''
104
- headers = hashPairs(ARGV[2...ARGV.length])
105
- $stderr.puts "delete ALL entries in #{bucket}:#{path} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
106
- more = true
107
- marker = nil
108
- while more do
109
- res = s3cmdList(bucket, path, nil, nil, marker)
110
- res.entries.each do |item|
111
- # the s3 commands (with my modified UTF-8 conversion) expect native char encoding input
112
- key = Iconv.iconv($S3SYNC_NATIVE_CHARSET, "UTF-8", item.key).join
113
- $stderr.puts "delete #{bucket}:#{key} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
114
- S3try(:delete, bucket, key) unless $S3syncOptions['--dryrun']
115
- end
116
- more = res.properties.is_truncated
117
- marker = (res.properties.next_marker)? res.properties.next_marker : ((res.entries.length > 0) ? res.entries.last.key : nil)
118
- # get this into local charset; when we pass it to s3 that is what's expected
119
- marker = Iconv.iconv($S3SYNC_NATIVE_CHARSET, "UTF-8", marker).join if marker
120
- end
121
- when "list"
122
- s3cmdUsage("Need a bucket") if bucket == ''
123
- max, delim = ARGV[2..3]
124
- headers = hashPairs(ARGV[4...ARGV.length])
125
- $stderr.puts "list #{bucket}:#{path} #{max} #{delim} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
126
- puts "--------------------"
127
-
128
- more = true
129
- marker = nil
130
- while more do
131
- res = s3cmdList(bucket, path, max, delim, marker, headers)
132
- if delim
133
- res.common_prefix_entries.each do |item|
134
-
135
- puts "dir: " + Iconv.iconv($S3SYNC_NATIVE_CHARSET, "UTF-8", item.prefix).join
136
- end
137
- puts "--------------------"
138
- end
139
- res.entries.each do |item|
140
- puts Iconv.iconv($S3SYNC_NATIVE_CHARSET, "UTF-8", item.key).join
141
- end
142
- if res.properties.is_truncated
143
- printf "More? Y/n: "
144
- more = (STDIN.gets.match('^[Yy]?$'))
145
- marker = (res.properties.next_marker)? res.properties.next_marker : ((res.entries.length > 0) ? res.entries.last.key : nil)
146
- # get this into local charset; when we pass it to s3 that is what's expected
147
- marker = Iconv.iconv($S3SYNC_NATIVE_CHARSET, "UTF-8", marker).join if marker
148
-
149
- else
150
- more = false
151
- end
152
- end # more
153
- when "listbuckets"
154
- headers = hashPairs(ARGV[1...ARGV.length])
155
- $stderr.puts "list all buckets #{headers.inspect if headers}" if $S3syncOptions['--verbose']
156
- if $S3syncOptions['--expires-in']
157
- $stdout.puts S3url(:list_all_my_buckets, headers)
158
- else
159
- res = S3try(:list_all_my_buckets, headers)
160
- res.entries.each do |item|
161
- puts item.name
162
- end
163
- end
164
- when "createbucket"
165
- s3cmdUsage("Need a bucket") if bucket == ''
166
- lc = ''
167
- if(ARGV.length > 2)
168
- lc = '<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01"><LocationConstraint>' + ARGV[2] + '</LocationConstraint></CreateBucketConfiguration>'
169
- end
170
- $stderr.puts "create bucket #{bucket} #{lc}" if $S3syncOptions['--verbose']
171
- S3try(:create_bucket, bucket, lc) unless $S3syncOptions['--dryrun']
172
- when "deletebucket"
173
- s3cmdUsage("Need a bucket") if bucket == ''
174
- headers = hashPairs(ARGV[2...ARGV.length])
175
- $stderr.puts "delete bucket #{bucket} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
176
- S3try(:delete_bucket, bucket, headers) unless $S3syncOptions['--dryrun']
177
- when "location"
178
- s3cmdUsage("Need a bucket") if bucket == ''
179
- headers = hashPairs(ARGV[2...ARGV.length])
180
- query = Hash.new
181
- query['location'] = 'location'
182
- $stderr.puts "location request bucket #{bucket} #{query.inspect} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
183
- S3try(:get_query_stream, bucket, '', query, headers, $stdout) unless $S3syncOptions['--dryrun']
184
- when "get"
185
- s3cmdUsage("Need a bucket") if bucket == ''
186
- s3cmdUsage("Need a key") if path == ''
187
- s3cmdUsage("Need a file") if file == ''
188
- headers = hashPairs(ARGV[3...ARGV.length])
189
- $stderr.puts "get from key #{bucket}:#{path} into #{file} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
190
- unless $S3syncOptions['--dryrun']
191
- if $S3syncOptions['--expires-in']
192
- $stdout.puts S3url(:get, bucket, path, headers)
193
- else
194
- outStream = File.open(file, 'wb')
195
- outStream = ProgressStream.new(outStream) if $S3syncOptions['--progress']
196
- S3try(:get_stream, bucket, path, headers, outStream)
197
- outStream.close
198
- end
199
- end
200
- when "put"
201
- s3cmdUsage("Need a bucket") if bucket == ''
202
- s3cmdUsage("Need a key") if path == ''
203
- s3cmdUsage("Need a file") if file == ''
204
- headers = hashPairs(ARGV[3...ARGV.length])
205
- stream = File.open(file, 'rb')
206
- stream = ProgressStream.new(stream, File.stat(file).size) if $S3syncOptions['--progress']
207
- s3o = S3::S3Object.new(stream, {}) # support meta later?
208
- headers['Content-Length'] = FileTest.size(file).to_s
209
- $stderr.puts "put to key #{bucket}:#{path} from #{file} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
210
- S3try(:put, bucket, path, s3o, headers) unless $S3syncOptions['--dryrun']
211
- stream.close
212
- else
213
- s3cmdUsage
214
- end
215
-
216
- end #main
217
- def S3sync.s3cmdList(bucket, path, max=nil, delim=nil, marker=nil, headers={})
218
- debug(max)
219
- options = Hash.new
220
- options['prefix'] = path # start at the right depth
221
- options['max-keys'] = max ? max.to_s : 100
222
- options['delimiter'] = delim if delim
223
- options['marker'] = marker if marker
224
- S3try(:list_bucket, bucket, options, headers)
225
- end
226
-
227
- # turn an array into a hash of pairs
228
- def S3sync.hashPairs(ar)
229
- ret = Hash.new
230
- ar.each do |item|
231
- name = (/^(.*?):/.match(item))[1]
232
- item = (/^.*?:(.*)$/.match(item))[1]
233
- ret[name] = item
234
- end if ar
235
- ret
236
- end
237
- end #module
238
-
239
-
240
-
241
- def debug(str)
242
- $stderr.puts str if $S3syncOptions['--debug']
243
- end
244
-
245
- S3sync::s3cmdMain #go!
@@ -1,103 +0,0 @@
1
- # This software code is made available "AS IS" without warranties of any
2
- # kind. You may copy, display, modify and redistribute the software
3
- # code either by itself or as incorporated into your code; provided that
4
- # you do not remove any proprietary notices. Your use of this software
5
- # code is at your own risk and you waive any claim against the author
6
- # with respect to your use of this software code.
7
- # (c) 2007 s3sync.net
8
- #
9
-
10
- # The purpose of this file is to overlay the net/http library
11
- # to add some functionality
12
- # (without changing the file itself or requiring a specific version)
13
- # It still isn't perfectly robust, i.e. if radical changes are made
14
- # to the underlying lib this stuff will need updating.
15
-
16
- require 'net/http'
17
-
18
- module Net
19
-
20
- $HTTPStreamingDebug = false
21
-
22
- # Allow request body to be an IO stream
23
- # Allow an IO stream argument to stream the response body out
24
- class HTTP
25
- alias _HTTPStreaming_request request
26
-
27
- def request(req, body = nil, streamResponseBodyTo = nil, &block)
28
- if not block_given? and streamResponseBodyTo and streamResponseBodyTo.respond_to?(:write)
29
- $stderr.puts "Response using streaming" if $HTTPStreamingDebug
30
- # this might be a retry, we should make sure the stream is at its beginning
31
- streamResponseBodyTo.rewind if streamResponseBodyTo.respond_to?(:rewind) and streamResponseBodyTo != $stdout
32
- block = proc do |res|
33
- res.read_body do |chunk|
34
- streamResponseBodyTo.write(chunk)
35
- end
36
- end
37
- end
38
- if body != nil && body.respond_to?(:read)
39
- $stderr.puts "Request using streaming" if $HTTPStreamingDebug
40
- # this might be a retry, we should make sure the stream is at its beginning
41
- body.rewind if body.respond_to?(:rewind)
42
- req.body_stream = body
43
- return _HTTPStreaming_request(req, nil, &block)
44
- else
45
- return _HTTPStreaming_request(req, body, &block)
46
- end
47
- end
48
- end
49
-
50
- end #module
51
-
52
- module S3sync
53
- class ProgressStream < SimpleDelegator
54
- def initialize(s, size=0)
55
- @start = @last = Time.new
56
- @total = size
57
- @transferred = 0
58
- @closed = false
59
- @printed = false
60
- @innerStream = s
61
- super(@innerStream)
62
- __setobj__(@innerStream)
63
- end
64
- # need to catch reads and writes so we can count what's being transferred
65
- def read(i)
66
- res = @innerStream.read(i)
67
- @transferred += res.respond_to?(:length) ? res.length : 0
68
- now = Time.new
69
- if(now - @last > 1) # don't do this oftener than once per second
70
- @printed = true
71
- $stdout.printf("\rProgress: %db %db/s %s ", @transferred, (@transferred/(now - @start)).floor,
72
- @total > 0? (100 * @transferred/@total).floor.to_s + "%" : ""
73
- )
74
- $stdout.flush
75
- @last = now
76
- end
77
- res
78
- end
79
- def write(s)
80
- @transferred += s.length
81
- res = @innerStream.write(s)
82
- now = Time.new
83
- if(now -@last > 1) # don't do this oftener than once per second
84
- @printed = true
85
- $stdout.printf("\rProgress: %db %db/s %s ", @transferred, (@transferred/(now - @start)).floor,
86
- @total > 0? (100 * @transferred/@total).floor.to_s + "%" : ""
87
- )
88
- $stdout.flush
89
- @last = now
90
- end
91
- res
92
- end
93
- def rewind()
94
- @transferred = 0
95
- @innerStream.rewind if @innerStream.respond_to?(:rewind)
96
- end
97
- def close()
98
- $stdout.printf("\n") if @printed and not @closed
99
- @closed = true
100
- @innerStream.close
101
- end
102
- end
103
- end #module
data/lib/S3.rb DELETED
@@ -1,707 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- # This software code is made available "AS IS" without warranties of any
4
- # kind. You may copy, display, modify and redistribute the software
5
- # code either by itself or as incorporated into your code; provided that
6
- # you do not remove any proprietary notices. Your use of this software
7
- # code is at your own risk and you waive any claim against Amazon
8
- # Digital Services, Inc. or its affiliates with respect to your use of
9
- # this software code. (c) 2006 Amazon Digital Services, Inc. or its
10
- # affiliates.
11
-
12
- require 'base64'
13
- require 'cgi'
14
- require 'openssl'
15
- require 'digest/sha1'
16
- require 'net/https'
17
- require 'rexml/document'
18
- require 'time'
19
-
20
- # this wasn't added until v 1.8.3
21
- if (RUBY_VERSION < '1.8.3')
22
- class Net::HTTP::Delete < Net::HTTPRequest
23
- METHOD = 'DELETE'
24
- REQUEST_HAS_BODY = false
25
- RESPONSE_HAS_BODY = true
26
- end
27
- end
28
-
29
- # this module has two big classes: AWSAuthConnection and
30
- # QueryStringAuthGenerator. both use identical apis, but the first actually
31
- # performs the operation, while the second simply outputs urls with the
32
- # appropriate authentication query string parameters, which could be used
33
- # in another tool (such as your web browser for GETs).
34
- module S3
35
- DEFAULT_HOST = 's3.amazonaws.com'
36
- PORTS_BY_SECURITY = { true => 443, false => 80 }
37
- METADATA_PREFIX = 'x-amz-meta-'
38
- AMAZON_HEADER_PREFIX = 'x-amz-'
39
-
40
- # builds the canonical string for signing.
41
- def S3.canonical_string(method, bucket="", path="", path_args={}, headers={}, expires=nil)
42
- interesting_headers = {}
43
- headers.each do |key, value|
44
- lk = key.downcase
45
- if (lk == 'content-md5' or
46
- lk == 'content-type' or
47
- lk == 'date' or
48
- lk =~ /^#{AMAZON_HEADER_PREFIX}/o)
49
- interesting_headers[lk] = value.to_s.strip
50
- end
51
- end
52
-
53
- # these fields get empty strings if they don't exist.
54
- interesting_headers['content-type'] ||= ''
55
- interesting_headers['content-md5'] ||= ''
56
-
57
- # just in case someone used this. it's not necessary in this lib.
58
- if interesting_headers.has_key? 'x-amz-date'
59
- interesting_headers['date'] = ''
60
- end
61
-
62
- # if you're using expires for query string auth, then it trumps date
63
- # (and x-amz-date)
64
- if not expires.nil?
65
- interesting_headers['date'] = expires
66
- end
67
-
68
- buf = "#{method}\n"
69
- interesting_headers.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
70
- if key =~ /^#{AMAZON_HEADER_PREFIX}/o
71
- buf << "#{key}:#{value}\n"
72
- else
73
- buf << "#{value}\n"
74
- end
75
- end
76
-
77
- # build the path using the bucket and key
78
- if not bucket.empty?
79
- buf << "/#{bucket}"
80
- end
81
- # append the key (it might be empty string)
82
- # append a slash regardless
83
- buf << "/#{path}"
84
-
85
- # if there is an acl, logging, or torrent parameter
86
- # add them to the string
87
- if path_args.has_key?('acl')
88
- buf << '?acl'
89
- elsif path_args.has_key?('torrent')
90
- buf << '?torrent'
91
- elsif path_args.has_key?('location')
92
- buf << '?location'
93
- elsif path_args.has_key?('logging')
94
- buf << '?logging'
95
- end
96
-
97
- return buf
98
- end
99
-
100
- # encodes the given string with the aws_secret_access_key, by taking the
101
- # hmac-sha1 sum, and then base64 encoding it. optionally, it will also
102
- # url encode the result of that to protect the string if it's going to
103
- # be used as a query string parameter.
104
- def S3.encode(aws_secret_access_key, str, urlencode=false)
105
- digest = OpenSSL::Digest::Digest.new('sha1')
106
- b64_hmac =
107
- Base64.encode64(
108
- OpenSSL::HMAC.digest(digest, aws_secret_access_key, str)).strip
109
-
110
- if urlencode
111
- return CGI::escape(b64_hmac)
112
- else
113
- return b64_hmac
114
- end
115
- end
116
-
117
- # build the path_argument string
118
- def S3.path_args_hash_to_string(path_args={})
119
- arg_string = ''
120
- path_args.each { |k, v|
121
- arg_string << k
122
- if not v.nil?
123
- arg_string << "=#{CGI::escape(v)}"
124
- end
125
- arg_string << '&'
126
- }
127
- return arg_string
128
- end
129
-
130
-
131
- # uses Net::HTTP to interface with S3. note that this interface should only
132
- # be used for smaller objects, as it does not stream the data. if you were
133
- # to download a 1gb file, it would require 1gb of memory. also, this class
134
- # creates a new http connection each time. it would be greatly improved with
135
- # some connection pooling.
136
- class AWSAuthConnection
137
- attr_accessor :calling_format
138
-
139
- def initialize(aws_access_key_id, aws_secret_access_key, is_secure=true,
140
- server=DEFAULT_HOST, port=PORTS_BY_SECURITY[is_secure],
141
- calling_format=CallingFormat::REGULAR)
142
- @aws_access_key_id = aws_access_key_id
143
- @aws_secret_access_key = aws_secret_access_key
144
- @server = server
145
- @is_secure = is_secure
146
- @calling_format = calling_format
147
- @port = port
148
- end
149
-
150
- def create_bucket(bucket, headers={})
151
- return Response.new(make_request('PUT', bucket, '', {}, headers))
152
- end
153
-
154
- # takes options :prefix, :marker, :max_keys, and :delimiter
155
- def list_bucket(bucket, options={}, headers={})
156
- path_args = {}
157
- options.each { |k, v|
158
- path_args[k] = v.to_s
159
- }
160
-
161
- return ListBucketResponse.new(make_request('GET', bucket, '', path_args, headers))
162
- end
163
-
164
- def delete_bucket(bucket, headers={})
165
- return Response.new(make_request('DELETE', bucket, '', {}, headers))
166
- end
167
-
168
- def put(bucket, key, object, headers={})
169
- object = S3Object.new(object) if not object.instance_of? S3Object
170
-
171
- return Response.new(
172
- make_request('PUT', bucket, CGI::escape(key), {}, headers, object.data, object.metadata)
173
- )
174
- end
175
-
176
- def get(bucket, key, headers={})
177
- return GetResponse.new(make_request('GET', bucket, CGI::escape(key), {}, headers))
178
- end
179
-
180
- def delete(bucket, key, headers={})
181
- return Response.new(make_request('DELETE', bucket, CGI::escape(key), {}, headers))
182
- end
183
-
184
- def head(bucket, key, headers={})
185
- return GetResponse.new(make_request('HEAD', bucket, CGI::escape(key), {}, headers))
186
- end
187
-
188
- def get_bucket_logging(bucket, headers={})
189
- return GetResponse.new(make_request('GET', bucket, '', {'logging' => nil}, headers))
190
- end
191
-
192
- def put_bucket_logging(bucket, logging_xml_doc, headers={})
193
- return Response.new(make_request('PUT', bucket, '', {'logging' => nil}, headers, logging_xml_doc))
194
- end
195
-
196
- def get_bucket_acl(bucket, headers={})
197
- return get_acl(bucket, '', headers)
198
- end
199
-
200
- # returns an xml document representing the access control list.
201
- # this could be parsed into an object.
202
- def get_acl(bucket, key, headers={})
203
- return GetResponse.new(make_request('GET', bucket, CGI::escape(key), {'acl' => nil}, headers))
204
- end
205
-
206
- def put_bucket_acl(bucket, acl_xml_doc, headers={})
207
- return put_acl(bucket, '', acl_xml_doc, headers)
208
- end
209
-
210
- # sets the access control policy for the given resource. acl_xml_doc must
211
- # be a string in the acl xml format.
212
- def put_acl(bucket, key, acl_xml_doc, headers={})
213
- return Response.new(
214
- make_request('PUT', bucket, CGI::escape(key), {'acl' => nil}, headers, acl_xml_doc, {})
215
- )
216
- end
217
-
218
- def list_all_my_buckets(headers={})
219
- return ListAllMyBucketsResponse.new(make_request('GET', '', '', {}, headers))
220
- end
221
-
222
- private
223
- def make_request(method, bucket='', key='', path_args={}, headers={}, data='', metadata={})
224
-
225
- # build the domain based on the calling format
226
- server = ''
227
- if bucket.empty?
228
- # for a bucketless request (i.e. list all buckets)
229
- # revert to regular domain case since this operation
230
- # does not make sense for vanity domains
231
- server = @server
232
- elsif @calling_format == CallingFormat::SUBDOMAIN
233
- server = "#{bucket}.#{@server}"
234
- elsif @calling_format == CallingFormat::VANITY
235
- server = bucket
236
- else
237
- server = @server
238
- end
239
-
240
- # build the path based on the calling format
241
- path = ''
242
- if (not bucket.empty?) and (@calling_format == CallingFormat::REGULAR)
243
- path << "/#{bucket}"
244
- end
245
- # add the slash after the bucket regardless
246
- # the key will be appended if it is non-empty
247
- path << "/#{key}"
248
-
249
- # build the path_argument string
250
- # add the ? in all cases since
251
- # signature and credentials follow path args
252
- path << '?'
253
- path << S3.path_args_hash_to_string(path_args)
254
-
255
- http = Net::HTTP.new(server, @port)
256
- http.use_ssl = @is_secure
257
- http.start do
258
- req = method_to_request_class(method).new("#{path}")
259
-
260
- set_headers(req, headers)
261
- set_headers(req, metadata, METADATA_PREFIX)
262
-
263
- set_aws_auth_header(req, @aws_access_key_id, @aws_secret_access_key, bucket, key, path_args)
264
- if req.request_body_permitted?
265
- return http.request(req, data)
266
- else
267
- return http.request(req)
268
- end
269
- end
270
- end
271
-
272
- def method_to_request_class(method)
273
- case method
274
- when 'GET'
275
- return Net::HTTP::Get
276
- when 'PUT'
277
- return Net::HTTP::Put
278
- when 'DELETE'
279
- return Net::HTTP::Delete
280
- when 'HEAD'
281
- return Net::HTTP::Head
282
- else
283
- raise "Unsupported method #{method}"
284
- end
285
- end
286
-
287
- # set the Authorization header using AWS signed header authentication
288
- def set_aws_auth_header(request, aws_access_key_id, aws_secret_access_key, bucket='', key='', path_args={})
289
- # we want to fix the date here if it's not already been done.
290
- request['Date'] ||= Time.now.httpdate
291
-
292
- # ruby will automatically add a random content-type on some verbs, so
293
- # here we add a dummy one to 'supress' it. change this logic if having
294
- # an empty content-type header becomes semantically meaningful for any
295
- # other verb.
296
- request['Content-Type'] ||= ''
297
-
298
- canonical_string =
299
- S3.canonical_string(request.method, bucket, key, path_args, request.to_hash, nil)
300
- encoded_canonical = S3.encode(aws_secret_access_key, canonical_string)
301
-
302
- request['Authorization'] = "AWS #{aws_access_key_id}:#{encoded_canonical}"
303
- end
304
-
305
- def set_headers(request, headers, prefix='')
306
- headers.each do |key, value|
307
- request[prefix + key] = value
308
- end
309
- end
310
- end
311
-
312
-
313
- # This interface mirrors the AWSAuthConnection class above, but instead
314
- # of performing the operations, this class simply returns a url that can
315
- # be used to perform the operation with the query string authentication
316
- # parameters set.
317
- class QueryStringAuthGenerator
318
- attr_accessor :calling_format
319
- attr_accessor :expires
320
- attr_accessor :expires_in
321
- attr_reader :server
322
- attr_reader :port
323
-
324
- # by default, expire in 1 minute
325
- DEFAULT_EXPIRES_IN = 60
326
-
327
- def initialize(aws_access_key_id, aws_secret_access_key, is_secure=true,
328
- server=DEFAULT_HOST, port=PORTS_BY_SECURITY[is_secure],
329
- format=CallingFormat::REGULAR)
330
- @aws_access_key_id = aws_access_key_id
331
- @aws_secret_access_key = aws_secret_access_key
332
- @protocol = is_secure ? 'https' : 'http'
333
- @server = server
334
- @port = port
335
- @calling_format = format
336
- # by default expire
337
- @expires_in = DEFAULT_EXPIRES_IN
338
- end
339
-
340
- # set the expires value to be a fixed time. the argument can
341
- # be either a Time object or else seconds since epoch.
342
- def expires=(value)
343
- @expires = value
344
- @expires_in = nil
345
- end
346
-
347
- # set the expires value to expire at some point in the future
348
- # relative to when the url is generated. value is in seconds.
349
- def expires_in=(value)
350
- @expires_in = value
351
- @expires = nil
352
- end
353
-
354
- def create_bucket(bucket, headers={})
355
- return generate_url('PUT', bucket, '', {}, headers)
356
- end
357
-
358
- # takes options :prefix, :marker, :max_keys, and :delimiter
359
- def list_bucket(bucket, options={}, headers={})
360
- path_args = {}
361
- options.each { |k, v|
362
- path_args[k] = v.to_s
363
- }
364
- return generate_url('GET', bucket, '', path_args, headers)
365
- end
366
-
367
- def delete_bucket(bucket, headers={})
368
- return generate_url('DELETE', bucket, '', {}, headers)
369
- end
370
-
371
- # don't really care what object data is. it's just for conformance with the
372
- # other interface. If this doesn't work, check tcpdump to see if the client is
373
- # putting a Content-Type header on the wire.
374
- def put(bucket, key, object=nil, headers={})
375
- object = S3Object.new(object) if not object.instance_of? S3Object
376
- return generate_url('PUT', bucket, CGI::escape(key), {}, merge_meta(headers, object))
377
- end
378
-
379
- def get(bucket, key, headers={})
380
- return generate_url('GET', bucket, CGI::escape(key), {}, headers)
381
- end
382
-
383
- def delete(bucket, key, headers={})
384
- return generate_url('DELETE', bucket, CGI::escape(key), {}, headers)
385
- end
386
-
387
- def get_bucket_logging(bucket, headers={})
388
- return generate_url('GET', bucket, '', {'logging' => nil}, headers)
389
- end
390
-
391
- def put_bucket_logging(bucket, logging_xml_doc, headers={})
392
- return generate_url('PUT', bucket, '', {'logging' => nil}, headers)
393
- end
394
-
395
- def get_acl(bucket, key='', headers={})
396
- return generate_url('GET', bucket, CGI::escape(key), {'acl' => nil}, headers)
397
- end
398
-
399
- def get_bucket_acl(bucket, headers={})
400
- return get_acl(bucket, '', headers)
401
- end
402
-
403
- # don't really care what acl_xml_doc is.
404
- # again, check the wire for Content-Type if this fails.
405
- def put_acl(bucket, key, acl_xml_doc, headers={})
406
- return generate_url('PUT', bucket, CGI::escape(key), {'acl' => nil}, headers)
407
- end
408
-
409
- def put_bucket_acl(bucket, acl_xml_doc, headers={})
410
- return put_acl(bucket, '', acl_xml_doc, headers)
411
- end
412
-
413
- def list_all_my_buckets(headers={})
414
- return generate_url('GET', '', '', {}, headers)
415
- end
416
-
417
-
418
- private
419
- # generate a url with the appropriate query string authentication
420
- # parameters set.
421
- def generate_url(method, bucket="", key="", path_args={}, headers={})
422
- expires = 0
423
- if not @expires_in.nil?
424
- expires = Time.now.to_i + @expires_in
425
- elsif not @expires.nil?
426
- expires = @expires
427
- else
428
- raise "invalid expires state"
429
- end
430
-
431
- canonical_string =
432
- S3::canonical_string(method, bucket, key, path_args, headers, expires)
433
- encoded_canonical =
434
- S3::encode(@aws_secret_access_key, canonical_string)
435
-
436
- url = CallingFormat.build_url_base(@protocol, @server, @port, bucket, @calling_format)
437
-
438
- path_args["Signature"] = encoded_canonical.to_s
439
- path_args["Expires"] = expires.to_s
440
- path_args["AWSAccessKeyId"] = @aws_access_key_id.to_s
441
- arg_string = S3.path_args_hash_to_string(path_args)
442
-
443
- return "#{url}/#{key}?#{arg_string}"
444
- end
445
-
446
- def merge_meta(headers, object)
447
- final_headers = headers.clone
448
- if not object.nil? and not object.metadata.nil?
449
- object.metadata.each do |k, v|
450
- final_headers[METADATA_PREFIX + k] = v
451
- end
452
- end
453
- return final_headers
454
- end
455
- end
456
-
457
- class S3Object
458
- attr_accessor :data
459
- attr_accessor :metadata
460
- def initialize(data, metadata={})
461
- @data, @metadata = data, metadata
462
- end
463
- end
464
-
465
- # class for storing calling format constants
466
- module CallingFormat
467
- REGULAR = 0 # http://s3.amazonaws.com/bucket/key
468
- SUBDOMAIN = 1 # http://bucket.s3.amazonaws.com/key
469
- VANITY = 2 # http://<vanity_domain>/key -- vanity_domain resolves to s3.amazonaws.com
470
-
471
- # build the url based on the calling format, and bucket
472
- def CallingFormat.build_url_base(protocol, server, port, bucket, format)
473
- build_url_base = "#{protocol}://"
474
- if bucket.empty?
475
- build_url_base << "#{server}:#{port}"
476
- elsif format == SUBDOMAIN
477
- build_url_base << "#{bucket}.#{server}:#{port}"
478
- elsif format == VANITY
479
- build_url_base << "#{bucket}:#{port}"
480
- else
481
- build_url_base << "#{server}:#{port}/#{bucket}"
482
- end
483
- return build_url_base
484
- end
485
- end
486
-
487
- class Owner
488
- attr_accessor :id
489
- attr_accessor :display_name
490
- end
491
-
492
- class ListEntry
493
- attr_accessor :key
494
- attr_accessor :last_modified
495
- attr_accessor :etag
496
- attr_accessor :size
497
- attr_accessor :storage_class
498
- attr_accessor :owner
499
- end
500
-
501
- class ListProperties
502
- attr_accessor :name
503
- attr_accessor :prefix
504
- attr_accessor :marker
505
- attr_accessor :max_keys
506
- attr_accessor :delimiter
507
- attr_accessor :is_truncated
508
- attr_accessor :next_marker
509
- end
510
-
511
- class CommonPrefixEntry
512
- attr_accessor :prefix
513
- end
514
-
515
- # Parses the list bucket output into a list of ListEntry objects, and
516
- # a list of CommonPrefixEntry objects if applicable.
517
- class ListBucketParser
518
- attr_reader :properties
519
- attr_reader :entries
520
- attr_reader :common_prefixes
521
-
522
- def initialize
523
- reset
524
- end
525
-
526
- def tag_start(name, attributes)
527
- if name == 'ListBucketResult'
528
- @properties = ListProperties.new
529
- elsif name == 'Contents'
530
- @curr_entry = ListEntry.new
531
- elsif name == 'Owner'
532
- @curr_entry.owner = Owner.new
533
- elsif name == 'CommonPrefixes'
534
- @common_prefix_entry = CommonPrefixEntry.new
535
- end
536
- end
537
-
538
- # we have one, add him to the entries list
539
- def tag_end(name)
540
- # this prefix is the one we echo back from the request
541
- if name == 'Name'
542
- @properties.name = @curr_text
543
- elsif name == 'Prefix' and @is_echoed_prefix
544
- @properties.prefix = @curr_text
545
- @is_echoed_prefix = nil
546
- elsif name == 'Marker'
547
- @properties.marker = @curr_text
548
- elsif name == 'MaxKeys'
549
- @properties.max_keys = @curr_text.to_i
550
- elsif name == 'Delimiter'
551
- @properties.delimiter = @curr_text
552
- elsif name == 'IsTruncated'
553
- @properties.is_truncated = @curr_text == 'true'
554
- elsif name == 'NextMarker'
555
- @properties.next_marker = @curr_text
556
- elsif name == 'Contents'
557
- @entries << @curr_entry
558
- elsif name == 'Key'
559
- @curr_entry.key = @curr_text
560
- elsif name == 'LastModified'
561
- @curr_entry.last_modified = @curr_text
562
- elsif name == 'ETag'
563
- @curr_entry.etag = @curr_text
564
- elsif name == 'Size'
565
- @curr_entry.size = @curr_text.to_i
566
- elsif name == 'StorageClass'
567
- @curr_entry.storage_class = @curr_text
568
- elsif name == 'ID'
569
- @curr_entry.owner.id = @curr_text
570
- elsif name == 'DisplayName'
571
- @curr_entry.owner.display_name = @curr_text
572
- elsif name == 'CommonPrefixes'
573
- @common_prefixes << @common_prefix_entry
574
- elsif name == 'Prefix'
575
- # this is the common prefix for keys that match up to the delimiter
576
- @common_prefix_entry.prefix = @curr_text
577
- end
578
- @curr_text = ''
579
- end
580
-
581
- def text(text)
582
- @curr_text += text
583
- end
584
-
585
- def xmldecl(version, encoding, standalone)
586
- # ignore
587
- end
588
-
589
- # get ready for another parse
590
- def reset
591
- @is_echoed_prefix = true;
592
- @entries = []
593
- @curr_entry = nil
594
- @common_prefixes = []
595
- @common_prefix_entry = nil
596
- @curr_text = ''
597
- end
598
- end
599
-
600
- class Bucket
601
- attr_accessor :name
602
- attr_accessor :creation_date
603
- end
604
-
605
- class ListAllMyBucketsParser
606
- attr_reader :entries
607
-
608
- def initialize
609
- reset
610
- end
611
-
612
- def tag_start(name, attributes)
613
- if name == 'Bucket'
614
- @curr_bucket = Bucket.new
615
- end
616
- end
617
-
618
- # we have one, add him to the entries list
619
- def tag_end(name)
620
- if name == 'Bucket'
621
- @entries << @curr_bucket
622
- elsif name == 'Name'
623
- @curr_bucket.name = @curr_text
624
- elsif name == 'CreationDate'
625
- @curr_bucket.creation_date = @curr_text
626
- end
627
- @curr_text = ''
628
- end
629
-
630
- def text(text)
631
- @curr_text += text
632
- end
633
-
634
- def xmldecl(version, encoding, standalone)
635
- # ignore
636
- end
637
-
638
- # get ready for another parse
639
- def reset
640
- @entries = []
641
- @owner = nil
642
- @curr_bucket = nil
643
- @curr_text = ''
644
- end
645
- end
646
-
647
- class Response
648
- attr_reader :http_response
649
- def initialize(response)
650
- @http_response = response
651
- end
652
- end
653
-
654
- class GetResponse < Response
655
- attr_reader :object
656
- def initialize(response)
657
- super(response)
658
- metadata = get_aws_metadata(response)
659
- data = response.body
660
- @object = S3Object.new(data, metadata)
661
- end
662
-
663
- # parses the request headers and pulls out the s3 metadata into a hash
664
- def get_aws_metadata(response)
665
- metadata = {}
666
- response.each do |key, value|
667
- if key =~ /^#{METADATA_PREFIX}(.*)$/oi
668
- metadata[$1] = value
669
- end
670
- end
671
- return metadata
672
- end
673
- end
674
-
675
- class ListBucketResponse < Response
676
- attr_reader :properties
677
- attr_reader :entries
678
- attr_reader :common_prefix_entries
679
-
680
- def initialize(response)
681
- super(response)
682
- if response.is_a? Net::HTTPSuccess
683
- parser = ListBucketParser.new
684
- REXML::Document.parse_stream(response.body, parser)
685
- @properties = parser.properties
686
- @entries = parser.entries
687
- @common_prefix_entries = parser.common_prefixes
688
- else
689
- @entries = []
690
- end
691
- end
692
- end
693
-
694
- class ListAllMyBucketsResponse < Response
695
- attr_reader :entries
696
- def initialize(response)
697
- super(response)
698
- if response.is_a? Net::HTTPSuccess
699
- parser = ListAllMyBucketsParser.new
700
- REXML::Document.parse_stream(response.body, parser)
701
- @entries = parser.entries
702
- else
703
- @entries = []
704
- end
705
- end
706
- end
707
- end