s3sync 1.2.5 → 2.0.0

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/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