s33r 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE.txt ADDED
@@ -0,0 +1,9 @@
1
+ This software is distributed under the MIT license (see MIT-LICENSE).
2
+
3
+ It incorporates code from the mimetypes library (http://raa.ruby-lang.org/project/mime-types/),
4
+ which is under a compatible license (same as Ruby).
5
+
6
+ It is also heavily based on the sample Ruby code provided by Amazon
7
+ (http://developer.amazonwebservices.com/connect/entry.jspa?externalID=135&categoryID=47).
8
+
9
+ I picked up a couple of ideas from http://rubyforge.org/projects/rsh3ll/ too.
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (C) 2006 Elliot Smith
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README.txt ADDED
@@ -0,0 +1,16 @@
1
+ This is a first pass at a Ruby library for talking to Amazon's S3 service.
2
+
3
+ It is less complete than my previous effort (http://townx.org/blog/elliot/s3_rails), but more solid, more elegant,
4
+ and with a test suite (which uses RSpec - see http://rubyforge.org/projects/rspec - I haven't added RSpec as a gem
5
+ dependency (you can ignore the tests if you like), but for reference I developed the tests using 0.5.12).
6
+
7
+ To use it from inside your Ruby script:
8
+
9
+ require 's33r'
10
+
11
+ If you want to see an example program which uses the library, have a look at bin/s3cli.rb. This is a simple command
12
+ line client which you can use to post a file to S3 and email a link to the file to someone. Useful for sending
13
+ big files to people without clogging their email inbox with enormous files. It is also intended as a demonstration
14
+ of how to use the library. Full instructions are included at the top of the file.
15
+
16
+ By the way, I've tested this on Linux, but not on Windows or Mac.
data/bin/config.yml ADDED
@@ -0,0 +1,5 @@
1
+ access_key: 'youraccesskey'
2
+ secret_key: 'yoursecretkey'
3
+ bucket: 'name-of-bucket'
4
+ from_email: 'elliot@example.com'
5
+ to_email: 'elliot@example.com'
data/bin/s3cli.rb ADDED
@@ -0,0 +1,96 @@
1
+ # SIMPLE COMMAND LINE S3 CLIENT
2
+ # You need an Amazon S3 account to use this: get one at http://aws.amazon.com/
3
+ #
4
+ # Create a config. file called based on the config.yml example in this directory;
5
+ # then change the config_file setting below to set the path to your config. file.
6
+ # You will be prompted to create a bucket if your setting for bucket in the config.
7
+ # file is blank.
8
+ #
9
+ # Note that this script doesn't give you any means of deleting anything; you will
10
+ # need to write your own program to do this (I use my own s33r server,
11
+ # which I am currently re-implementing to work with this library).
12
+ #
13
+ # Call this script with two arguments:
14
+ # filename: file to upload; the file is uploaded to S3 and assigned a key
15
+ # based on filename supplied (including path)
16
+ # to_email: email address to send to (optional)
17
+
18
+ require 'yaml'
19
+ require 'rubygems'
20
+ require_gem 's33r'
21
+ require 'net/smtp'
22
+
23
+ filename = ARGV[0]
24
+ to_email = ARGV[1]
25
+ config_file = '/home/ell/.s33r'
26
+ if '/path/to/your/config/file' == config_file
27
+ puts 'Please set the config_file variable to the path to your config. file'
28
+ exit
29
+ end
30
+
31
+ # load config. file
32
+ options = YAML::load_file(config_file)
33
+ access_key = options['access_key']
34
+ secret_key = options['secret_key']
35
+ from_email = options['from_email']
36
+ to_email ||= options['to_email']
37
+ bucket = options['bucket']
38
+
39
+ # check for bucket
40
+ if !bucket
41
+ require 'readline'
42
+ bucket = Readline.readline('No bucket found; please enter name for new bucket: ', true)
43
+ client = S3::Client.new(access_key, secret_key)
44
+ response = client.create_bucket(bucket, client.canned_acl_header('public-read'))
45
+ if response.ok?
46
+ puts 'Created new bucket'
47
+ else
48
+ puts 'Could not create bucket (HTTP problem)'
49
+ exit
50
+ end
51
+ end
52
+
53
+ # estimate upload time
54
+ filesize = FileTest.size(filename)
55
+ filesize_mb = (filesize / (1000*1000)).to_i
56
+ secs_per_mb = 12
57
+ str = "Transferring file; size %d bytes" % filesize
58
+ str += " (about %d Mb)" % filesize_mb if filesize_mb > 0
59
+ str += "\nUploading normally takes at least %d seconds per Mb\n" % secs_per_mb
60
+
61
+ if filesize_mb > 0
62
+ str += "So this should take about %d seconds" % (filesize_mb * secs_per_mb)
63
+ else
64
+ str += "So this should be done in no time"
65
+ end
66
+
67
+ puts str
68
+
69
+ # time the put
70
+ start_time_secs = Time.now.to_i
71
+
72
+ # a client pointing at the specified bucket
73
+ S3::NamedBucket.new(access_key, secret_key, bucket, :public_contents => true) do |client|
74
+ response = client.put_file(filename)
75
+ end
76
+
77
+ time_taken_secs = Time.now.to_i - start_time_secs
78
+ puts "Aah, it appears to have taken %d seconds" % time_taken_secs
79
+
80
+ # post-put report
81
+ if response.ok?
82
+ puts "File #{filename} transferred OK"
83
+ url = File.join("http://", S3::HOST, client.bucket_name, '/') + filename
84
+ puts "Available at URL:"
85
+ puts url
86
+
87
+ if to_email
88
+ message = "From:#{from_email}\r\nTo:#{to_email}\r\nSubject:You were sent a file\r\n\r\nFetch it from\n#{url}"
89
+
90
+ Net::SMTP.start('localhost') do |smtp|
91
+ smtp.send_message message, from_email, to_email
92
+ end
93
+ end
94
+ else
95
+ puts 'File transfer failed'
96
+ end
data/lib/s33r.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 's33r/core'
2
+ require 's33r/client'
3
+ require 's33r/list_bucket_result'
4
+ require 's33r/net_http_overrides'
5
+ require 's33r/named_bucket'
6
+ require 's33r/s3_exception'
7
+ require 's33r/external/mimetypes'
@@ -0,0 +1,188 @@
1
+ require 'net/https'
2
+ require 'cgi'
3
+
4
+ # this is a very thin layer over the S3 API
5
+ # TODO: need to wrap XML returned into object representation
6
+ module S3
7
+ include Net
8
+
9
+ class Client
10
+ include S3
11
+ attr_accessor :chunk_size, :default_headers
12
+
13
+ def initialize(aws_access_key, aws_secret_access_key)
14
+ @client = HTTP.new(HOST, PORT)
15
+
16
+ # turn off SSL certificate verification
17
+ @client.verify_mode = OpenSSL::SSL::VERIFY_NONE
18
+
19
+ # always use SSL
20
+ @client.use_ssl = true
21
+
22
+ # set default chunk size for streaming request body (1 Mb)
23
+ @chunk_size = 1048576
24
+
25
+ # Amazon S3 developer keys
26
+ @aws_access_key = aws_access_key
27
+ @aws_secret_access_key = aws_secret_access_key
28
+
29
+ # headers sent with every request made by this client
30
+ @client_headers = {}
31
+ end
32
+
33
+ # send a request over the wire
34
+ def do_request(method, path, data=nil, headers={})
35
+ req = get_requester(method, path)
36
+ req.chunk_size = @chunk_size
37
+
38
+ # add the S3 headers which are always required
39
+ headers = add_default_headers(headers)
40
+
41
+ # add any client-specific default headers
42
+ headers = add_client_headers(headers)
43
+
44
+ headers['Authorization'] = generate_auth_header_value(method, path, headers,
45
+ @aws_access_key, @aws_secret_access_key)
46
+
47
+ headers.each do |key, value|
48
+ req[key] = value
49
+ end
50
+
51
+ @client.start do
52
+ if req.request_body_permitted?
53
+ # for streaming large files
54
+ if data.respond_to?(:read)
55
+ req.body_stream = data
56
+ req['Content-Length'] = data.stat.size.to_s
57
+ return @client.request(req, nil)
58
+ # simple text strings etc.
59
+ else
60
+ return @client.request(req, data)
61
+ end
62
+ else
63
+ return @client.request(req)
64
+ end
65
+ end
66
+
67
+ end
68
+
69
+ # get
70
+ def do_get(path='/', headers={})
71
+ do_request('GET', path, headers)
72
+ end
73
+
74
+ # head
75
+ def do_head(path='/', headers={})
76
+ do_request('HEAD', path, headers)
77
+ end
78
+
79
+ # post
80
+ def do_post(path='/', data=nil, headers={})
81
+ do_request('POST', path, data, headers)
82
+ end
83
+
84
+ # put
85
+ def do_put(path='/', data=nil, headers={})
86
+ do_request('PUT', path, data, headers)
87
+ end
88
+
89
+ # return an instance of an appropriate request class
90
+ def get_requester(method, path)
91
+ raise S3Exception::UnsupportedHTTPMethod, "The #{method} HTTP method is not supported" if !(METHOD_VERBS.include?(method))
92
+ eval("HTTP::" + method[0,1].upcase + method[1..-1].downcase + ".new('#{path}')")
93
+ end
94
+
95
+ # convert a hash of name/value pairs to querystring variables
96
+ def get_querystring(pairs={})
97
+ str = ''
98
+ if pairs.size > 0
99
+ str += "?" + pairs.map { |key, value| "#{key}=#{CGI::escape(value.to_s)}" }.join('&')
100
+ end
101
+ str
102
+ end
103
+
104
+ # list all buckets
105
+ def list_all_buckets
106
+ do_get('/')
107
+ end
108
+
109
+ # create a bucket
110
+ def create_bucket(bucket_name, headers={})
111
+ bucket_name_valid?(bucket_name)
112
+ bucket_exists?(bucket_name)
113
+ do_put("/#{bucket_name}", nil, headers)
114
+ end
115
+
116
+ # list entries in a bucket
117
+ def list_bucket(bucket_name)
118
+ bucket_name_valid?(bucket_name)
119
+ bucket_exists?(bucket_name)
120
+ do_get("/#{bucket_name}")
121
+ end
122
+
123
+ # put some resource onto S3
124
+ def put_resource(data, bucket_name, resource_key, headers={})
125
+ do_put(File.join("/#{bucket_name}", "#{CGI::escape(resource_key)}"), data, headers)
126
+ end
127
+
128
+ # put a string onto S3
129
+ def put_text(string, bucket_name, resource_key, headers={})
130
+ headers["Content-Type"] = "text/plain"
131
+ put_resource(bucket_name, resource_key, string, headers)
132
+ end
133
+
134
+ # put a file onto S3
135
+ def put_file(filename, bucket_name, resource_key=nil, headers={})
136
+ # default to the file path as the resource key if none explicitly set
137
+ if resource_key.nil?
138
+ resource_key = filename
139
+ end
140
+
141
+ # content type is explicitly set in the headers
142
+ if headers[:content_type]
143
+ # use the first MIME type corresponding to this content type string
144
+ # (MIME::Types returns an array of possible MIME types)
145
+ mime_type = MIME::Types[headers[:content_type]][0]
146
+ else
147
+ mime_type = guess_mime_type(filename)
148
+ end
149
+ content_type = mime_type.simplified
150
+ headers['Content-Type'] = content_type
151
+ headers['Content-Transfer-Encoding'] = 'binary' if mime_type.binary?
152
+
153
+ # the data we want to put (handle to file, so we can stream from it)
154
+ File.open(filename) do |data|
155
+ # send the put request
156
+ put_resource(data, bucket_name, resource_key, headers)
157
+ end
158
+ end
159
+
160
+ # guess a file's mime type
161
+ # NB if the mime_type for a file cannot be guessed, "text/plain" is used
162
+ def guess_mime_type(filename)
163
+ mime_type = MIME::Types.type_for(filename)[0]
164
+ mime_type ||= MIME::Types['text/plain'][0]
165
+ mime_type
166
+ end
167
+
168
+ # ensure that a bucket_name is well-formed
169
+ def bucket_name_valid?(bucket_name)
170
+ if '/' == bucket_name[0,1]
171
+ raise S3Exception::MalformedBucketName, "Bucket name cannot have a leading slash"
172
+ end
173
+ end
174
+
175
+ # TODO: proper check for existence of bucket;
176
+ # throw error if bucket does not exist (see bucket_name_valid? for example)
177
+ def bucket_exists?(bucket_name)
178
+ false
179
+ end
180
+
181
+ # add any default headers which should be sent with every request from the client;
182
+ # any headers passed into this method override the defaults in @client_headers
183
+ def add_client_headers(headers)
184
+ headers.merge!(@client_headers) { |key, arg, default| arg }
185
+ end
186
+
187
+ end
188
+ end
data/lib/s33r/core.rb ADDED
@@ -0,0 +1,131 @@
1
+ require 'base64'
2
+ require 'time'
3
+ require 'net/https'
4
+ require 'openssl'
5
+
6
+ module S3
7
+ HOST = 's3.amazonaws.com'
8
+ PORT = 443
9
+ METADATA_PREFIX = 'x-amz-meta-'
10
+ AWS_HEADER_PREFIX = 'x-amz-'
11
+ AWS_AUTH_HEADER_VALUE = "AWS %s:%s"
12
+ INTERESTING_HEADERS = ['content-md5', 'content-type', 'date']
13
+ REQUIRED_HEADERS = ['Content-Type', 'Date']
14
+ CANNED_ACLS = ['private', 'public-read', 'public-read-write', 'authenticated-read']
15
+ METHOD_VERBS = ['GET', 'PUT', 'HEAD', 'POST']
16
+
17
+ # builds the canonical string for signing;
18
+ # modified (slightly) from the Amazon sample code
19
+ def generate_canonical_string(method, path, headers={}, expires=nil)
20
+ interesting_headers = {}
21
+ headers.each do |key, value|
22
+ lk = key.downcase
23
+ if (INTERESTING_HEADERS.include?(lk) or lk =~ /^#{AWS_HEADER_PREFIX}/o)
24
+ interesting_headers[lk] = value
25
+ end
26
+ end
27
+
28
+ # these fields get empty strings if they don't exist.
29
+ interesting_headers['content-type'] ||= ''
30
+ interesting_headers['content-md5'] ||= ''
31
+
32
+ # if you're using expires for query string auth, then it trumps date
33
+ if not expires.nil?
34
+ interesting_headers['date'] = expires
35
+ end
36
+
37
+ buf = "#{method}\n"
38
+ interesting_headers.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
39
+ if key =~ /^#{AWS_HEADER_PREFIX}/o
40
+ buf << "#{key}:#{value}\n"
41
+ else
42
+ buf << "#{value}\n"
43
+ end
44
+ end
45
+
46
+ # ignore everything after the question mark...
47
+ buf << path.gsub(/\?.*$/, '')
48
+
49
+ # ...unless there is an acl or torrent parameter
50
+ if path =~ /[&?]acl($|&|=)/
51
+ buf << '?acl'
52
+ elsif path =~ /[&?]torrent($|&|=)/
53
+ buf << '?torrent'
54
+ end
55
+
56
+ return buf
57
+ end
58
+
59
+ # get the header value for AWS authentication
60
+ def generate_auth_header_value(method, path, headers, aws_access_key, aws_secret_access_key)
61
+ raise S3Exception::MethodNotAvailable, "Method %s not available" % method if !METHOD_VERBS.include?(method)
62
+
63
+ # check the headers needed for authentication have been set
64
+ missing_headers = REQUIRED_HEADERS - headers.keys
65
+ if !(missing_headers.empty?)
66
+ raise S3Exception::MissingRequiredHeaders,
67
+ "Headers required for AWS auth value are missing: " + missing_headers.join(', ')
68
+ end
69
+
70
+ # get the AWS header
71
+ canonical_string = generate_canonical_string(method, path, headers)
72
+ signature = generate_signature(aws_secret_access_key, canonical_string)
73
+ AWS_AUTH_HEADER_VALUE % [aws_access_key, signature]
74
+ end
75
+
76
+ # encode the given string with the aws_secret_access_key, by taking the
77
+ # hmac sha1 sum, and then base64 encoding it
78
+ def generate_signature(aws_secret_access_key, str, urlencode=false)
79
+ digest = OpenSSL::HMAC::digest(OpenSSL::Digest::Digest.new("SHA1"), aws_secret_access_key, str)
80
+ Base64.encode64(digest).strip
81
+ end
82
+
83
+ # build the headers required with every S3 request
84
+ # options hash can contain extra header settings, as follows:
85
+ # :date and :content_type are required headers, and set to defaults if not supplied
86
+ def add_default_headers(headers, options={})
87
+
88
+ # set the default headers required by AWS
89
+ missing_headers = REQUIRED_HEADERS - headers.keys
90
+
91
+ if missing_headers.include?('Content-Type')
92
+ content_type = options[:content_type] || ''
93
+ headers['Content-Type'] = content_type
94
+ end
95
+
96
+ if missing_headers.include?('Date')
97
+ date = options[:date] || Time.now
98
+ headers['Date'] = date.httpdate
99
+ end
100
+
101
+ headers
102
+ end
103
+
104
+ # add metadata headers to the request, correctly prefixing them first
105
+ # returns the headers with the metadata headers appended
106
+ def metadata_headers(headers, metadata={})
107
+ unless metadata.empty?
108
+ metadata.each { |key, value| headers[METADATA_PREFIX + key] = value }
109
+ end
110
+ headers
111
+ end
112
+
113
+ # add a canned ACL setter
114
+ def canned_acl_header(canned_acl, headers={})
115
+ unless canned_acl.nil?
116
+ unless CANNED_ACLS.include?(canned_acl)
117
+ raise S3Exception::UnsupportedCannedACL, "The canned ACL #{canned_acl} is not supported"
118
+ end
119
+ headers[AWS_HEADER_PREFIX + 'acl'] = canned_acl
120
+ end
121
+ headers
122
+ end
123
+
124
+ # guess a file's mime type
125
+ # NB if the mime_type for a file cannot be guessed, "text/plain" is used
126
+ def guess_mime_type(file_name)
127
+ mime_type = MIME::Types.type_for(file_name)[0]
128
+ mime_type = MIME::Types['text/plain'][0] unless mime_type
129
+ mime_type
130
+ end
131
+ end