s33r 0.4.2 → 0.5
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/examples/cli/instant_download_server.rb +88 -0
- data/examples/cli/s3cli.rb +31 -52
- data/examples/cli/simple.rb +16 -6
- data/examples/fores33r/app/controllers/browser_controller.rb +12 -10
- data/examples/fores33r/app/helpers/application_helper.rb +2 -1
- data/examples/fores33r/app/views/browser/_upload.rhtml +1 -1
- data/examples/fores33r/app/views/browser/index.rhtml +4 -4
- data/examples/fores33r/config/environment.rb +5 -3
- data/examples/fores33r/log/development.log +2259 -0
- data/examples/fores33r/log/mongrel.log +59 -0
- data/examples/s3.yaml +2 -6
- data/lib/s33r/bucket.rb +103 -0
- data/lib/s33r/bucket_listing.rb +33 -76
- data/lib/s33r/client.rb +305 -446
- data/lib/s33r/networking.rb +197 -0
- data/lib/s33r/s33r_exception.rb +29 -18
- data/lib/s33r/s33r_http.rb +36 -18
- data/lib/s33r/s3_acl.rb +32 -52
- data/lib/s33r/s3_logging.rb +117 -0
- data/lib/s33r/s3_obj.rb +124 -69
- data/lib/s33r/utility.rb +447 -0
- data/test/cases/spec_acl.rb +10 -40
- data/test/cases/spec_bucket_listing.rb +12 -32
- data/test/cases/spec_logging.rb +47 -0
- data/test/cases/spec_networking.rb +11 -0
- data/test/cases/spec_s3_object.rb +44 -5
- data/test/cases/spec_utility.rb +264 -0
- data/test/files/acl.xml +0 -6
- data/test/files/config.yaml +5 -0
- data/test/files/logging_status_disabled.xml +3 -0
- data/test/files/logging_status_enabled.xml +7 -0
- data/test/test_setup.rb +7 -2
- metadata +16 -94
- data/examples/cli/acl_x.rb +0 -41
- data/examples/cli/logging_x.rb +0 -20
- data/examples/fores33r/README +0 -183
- data/html/classes/MIME.html +0 -120
- data/html/classes/MIME/InvalidContentType.html +0 -119
- data/html/classes/MIME/Type.html +0 -1173
- data/html/classes/MIME/Types.html +0 -566
- data/html/classes/Net.html +0 -108
- data/html/classes/Net/HTTPGenericRequest.html +0 -233
- data/html/classes/Net/HTTPResponse.html +0 -271
- data/html/classes/S33r.html +0 -986
- data/html/classes/S33r/BucketListing.html +0 -434
- data/html/classes/S33r/Client.html +0 -1575
- data/html/classes/S33r/LoggingResource.html +0 -222
- data/html/classes/S33r/NamedBucket.html +0 -693
- data/html/classes/S33r/OrderlyXmlMarkup.html +0 -165
- data/html/classes/S33r/S33rException.html +0 -124
- data/html/classes/S33r/S33rException/BucketListingMaxKeysError.html +0 -111
- data/html/classes/S33r/S33rException/BucketNotLogTargetable.html +0 -119
- data/html/classes/S33r/S33rException/InvalidBucketListing.html +0 -111
- data/html/classes/S33r/S33rException/InvalidPermission.html +0 -111
- data/html/classes/S33r/S33rException/InvalidS3GroupType.html +0 -111
- data/html/classes/S33r/S33rException/MalformedBucketName.html +0 -111
- data/html/classes/S33r/S33rException/MethodNotAvailable.html +0 -111
- data/html/classes/S33r/S33rException/MissingBucketName.html +0 -111
- data/html/classes/S33r/S33rException/MissingRequiredHeaders.html +0 -111
- data/html/classes/S33r/S33rException/MissingResource.html +0 -111
- data/html/classes/S33r/S33rException/S3FallenOver.html +0 -111
- data/html/classes/S33r/S33rException/TryingToPutEmptyResource.html +0 -117
- data/html/classes/S33r/S33rException/UnsupportedCannedACL.html +0 -111
- data/html/classes/S33r/S33rException/UnsupportedHTTPMethod.html +0 -111
- data/html/classes/S33r/S3ACL.html +0 -125
- data/html/classes/S33r/S3ACL/ACLDoc.html +0 -521
- data/html/classes/S33r/S3ACL/AmazonCustomer.html +0 -168
- data/html/classes/S33r/S3ACL/CanonicalUser.html +0 -212
- data/html/classes/S33r/S3ACL/Grant.html +0 -403
- data/html/classes/S33r/S3ACL/Grantee.html +0 -239
- data/html/classes/S33r/S3ACL/Group.html +0 -178
- data/html/classes/S33r/S3Object.html +0 -618
- data/html/classes/S33r/Sync.html +0 -152
- data/html/classes/XML.html +0 -202
- data/html/classes/XML/Document.html +0 -125
- data/html/classes/XML/Node.html +0 -124
- data/html/created.rid +0 -1
- data/html/files/CHANGELOG.html +0 -107
- data/html/files/MIT-LICENSE.html +0 -129
- data/html/files/README_txt.html +0 -259
- data/html/files/lib/s33r/bucket_listing_rb.html +0 -101
- data/html/files/lib/s33r/builder_rb.html +0 -108
- data/html/files/lib/s33r/client_rb.html +0 -111
- data/html/files/lib/s33r/core_rb.html +0 -113
- data/html/files/lib/s33r/libxml_extensions_rb.html +0 -101
- data/html/files/lib/s33r/libxml_loader_rb.html +0 -109
- data/html/files/lib/s33r/logging_rb.html +0 -108
- data/html/files/lib/s33r/mimetypes_rb.html +0 -120
- data/html/files/lib/s33r/named_bucket_rb.html +0 -101
- data/html/files/lib/s33r/s33r_exception_rb.html +0 -101
- data/html/files/lib/s33r/s33r_http_rb.html +0 -108
- data/html/files/lib/s33r/s3_acl_rb.html +0 -108
- data/html/files/lib/s33r/s3_obj_rb.html +0 -108
- data/html/files/lib/s33r/sync_rb.html +0 -101
- data/html/files/lib/s33r_rb.html +0 -101
- data/html/fr_class_index.html +0 -66
- data/html/fr_file_index.html +0 -44
- data/html/fr_method_index.html +0 -183
- data/html/index.html +0 -24
- data/html/rdoc-style.css +0 -208
- data/lib/s33r/core.rb +0 -296
- data/lib/s33r/logging.rb +0 -43
- data/lib/s33r/named_bucket.rb +0 -148
- data/lib/s33r/sync.rb +0 -13
- data/test/cases/spec_all_buckets.rb +0 -28
- data/test/cases/spec_client.rb +0 -101
- data/test/cases/spec_core.rb +0 -128
- data/test/cases/spec_namedbucket.rb +0 -46
- data/test/cases/spec_sync.rb +0 -34
- data/test/files/all_buckets.xml +0 -21
- data/test/files/client_config.yml +0 -5
- data/test/files/namedbucket_config.yml +0 -8
- data/test/files/namedbucket_config2.yml +0 -8
- data/test/test_bucket_setup.rb +0 -41
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
** Daemonized, any open files are closed. Look at /home/ell/dev/s3/s33r/examples/fores33r/tmp/fores33r.pid and log/mongrel.log for info.
|
|
2
|
+
** Starting Mongrel listening at 0.0.0.0:3333
|
|
3
|
+
** Starting Rails with development environment...
|
|
4
|
+
/opt/lampp/lib/ruby/gems/1.8/gems/activesupport-1.3.1/lib/active_support/dependencies.rb:123:in `const_missing': uninitialized constant Client (NameError)
|
|
5
|
+
from /home/ell/dev/s3/s33r/examples/fores33r/config/environment.rb:62
|
|
6
|
+
from /opt/lampp/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
|
|
7
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/rails.rb:161:in `rails'
|
|
8
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:112:in `cloaker_'
|
|
9
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/configurator.rb:134:in `listener'
|
|
10
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:98:in `cloaker_'
|
|
11
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/configurator.rb:51:in `initialize'
|
|
12
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:85:in `run'
|
|
13
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/command.rb:211:in `run'
|
|
14
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:231
|
|
15
|
+
from /opt/lampp/bin/mongrel_rails:18
|
|
16
|
+
** Daemonized, any open files are closed. Look at /home/ell/dev/s3/s33r/examples/fores33r/tmp/fores33r.pid and log/mongrel.log for info.
|
|
17
|
+
** Starting Mongrel listening at 0.0.0.0:3333
|
|
18
|
+
** Starting Rails with development environment...
|
|
19
|
+
/opt/lampp/lib/ruby/gems/1.8/gems/activesupport-1.3.1/lib/active_support/dependencies.rb:123:in `const_missing': uninitialized constant Client (NameError)
|
|
20
|
+
from /home/ell/dev/s3/s33r/examples/fores33r/config/environment.rb:62
|
|
21
|
+
from /opt/lampp/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
|
|
22
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/rails.rb:161:in `rails'
|
|
23
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:112:in `cloaker_'
|
|
24
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/configurator.rb:134:in `listener'
|
|
25
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:98:in `cloaker_'
|
|
26
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/configurator.rb:51:in `initialize'
|
|
27
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:85:in `run'
|
|
28
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/command.rb:211:in `run'
|
|
29
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:231
|
|
30
|
+
from /opt/lampp/bin/mongrel_rails:18
|
|
31
|
+
** Daemonized, any open files are closed. Look at /home/ell/dev/s3/s33r/examples/fores33r/tmp/fores33r.pid and log/mongrel.log for info.
|
|
32
|
+
** Starting Mongrel listening at 0.0.0.0:3333
|
|
33
|
+
** Starting Rails with development environment...
|
|
34
|
+
/opt/lampp/lib/ruby/gems/1.8/gems/activesupport-1.3.1/lib/active_support/dependencies.rb:123:in `const_missing': uninitialized constant Client (NameError)
|
|
35
|
+
from /home/ell/dev/s3/s33r/examples/fores33r/config/environment.rb:62
|
|
36
|
+
from /opt/lampp/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
|
|
37
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/rails.rb:161:in `rails'
|
|
38
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:112:in `cloaker_'
|
|
39
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/configurator.rb:134:in `listener'
|
|
40
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:98:in `cloaker_'
|
|
41
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/configurator.rb:51:in `initialize'
|
|
42
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:85:in `run'
|
|
43
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/command.rb:211:in `run'
|
|
44
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:231
|
|
45
|
+
from /opt/lampp/bin/mongrel_rails:18
|
|
46
|
+
** Daemonized, any open files are closed. Look at /home/ell/dev/s3/s33r/examples/fores33r/tmp/fores33r.pid and log/mongrel.log for info.
|
|
47
|
+
** Starting Mongrel listening at 0.0.0.0:3333
|
|
48
|
+
** Starting Rails with development environment...
|
|
49
|
+
/home/ell/dev/s3/s33r/examples/fores33r/config/environment.rb:62: undefined method `load_config' for S33r::Client:Class (NoMethodError)
|
|
50
|
+
from /opt/lampp/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
|
|
51
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/rails.rb:161:in `rails'
|
|
52
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:112:in `cloaker_'
|
|
53
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/configurator.rb:134:in `listener'
|
|
54
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:98:in `cloaker_'
|
|
55
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/configurator.rb:51:in `initialize'
|
|
56
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:85:in `run'
|
|
57
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/lib/mongrel/command.rb:211:in `run'
|
|
58
|
+
from /opt/lampp/lib/ruby/gems/1.8/gems/mongrel-0.3.13.4/bin/mongrel_rails:231
|
|
59
|
+
from /opt/lampp/bin/mongrel_rails:18
|
data/examples/s3.yaml
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
# note you can use ERB code in this file
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
access: 'yourkey'
|
|
3
|
+
secret: 'yoursecretkey'
|
|
4
4
|
options:
|
|
5
5
|
default_expires: '12th November 2033'
|
|
6
6
|
use_ssl: true
|
|
7
7
|
dump_requests: false
|
|
8
|
-
default_prefix: 'someprefix'
|
|
9
|
-
default_bucket: 'somebucket'
|
|
10
|
-
from_email: 'elliot@example.com'
|
|
11
|
-
to_email: 'elliot@example.com'
|
data/lib/s33r/bucket.rb
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
base = File.dirname(__FILE__)
|
|
2
|
+
require File.join(base, 'utility')
|
|
3
|
+
|
|
4
|
+
# These methods are added to any Client with a bucket
|
|
5
|
+
# binding set.
|
|
6
|
+
module S33r
|
|
7
|
+
class Bucket < Client
|
|
8
|
+
|
|
9
|
+
attr_accessor :name
|
|
10
|
+
|
|
11
|
+
# +options+:
|
|
12
|
+
# * <tt>:check => true</tt>: if setting a :bucket option, the default behaviour is not to check
|
|
13
|
+
# whether the bucket actually exists on S3. If you pass this option,
|
|
14
|
+
# S33r will only set the bucket if it is on S3 and accessible;
|
|
15
|
+
# if it isn't, an error is raised (NoSuchBucket).
|
|
16
|
+
# * <tt>:create => true</tt>: if the bucket doesn't exist, try to create it.
|
|
17
|
+
# S33r will check before trying to create the bucket and will just return the
|
|
18
|
+
# bucket if it does.
|
|
19
|
+
def initialize(bucket_name, options={})
|
|
20
|
+
super(options)
|
|
21
|
+
|
|
22
|
+
if options[:create]
|
|
23
|
+
options[:bucket] = bucket_name
|
|
24
|
+
raise last_response.s3_error unless do_put(nil, options).ok?
|
|
25
|
+
end
|
|
26
|
+
if options[:check]
|
|
27
|
+
raise InvalidBucket, "Bucket #{name} does not exist" unless bucket_exists?(bucket_name)
|
|
28
|
+
end
|
|
29
|
+
@name = bucket_name
|
|
30
|
+
|
|
31
|
+
yield self if block_given?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Defaults for every request.
|
|
35
|
+
def request_defaults
|
|
36
|
+
super.merge(:bucket => name)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def exists?
|
|
40
|
+
bucket_exists?(name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Destroy this bucket.
|
|
44
|
+
#
|
|
45
|
+
# Pass <tt>:force => true</tt> to delete the content of
|
|
46
|
+
# the bucket if it exists.
|
|
47
|
+
def destroy(options={})
|
|
48
|
+
delete_bucket(name, options)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Delete a key from inside the bucket.
|
|
52
|
+
def delete(key, options={})
|
|
53
|
+
options[:key] = key
|
|
54
|
+
do_delete(options).ok?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# List of keys in the bucket.
|
|
58
|
+
#
|
|
59
|
+
# +options+ are passed through to Client.listing.
|
|
60
|
+
#-- TODO: tests
|
|
61
|
+
def keys(options={})
|
|
62
|
+
listing(options).keys.sort
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get an object from S3.
|
|
66
|
+
#
|
|
67
|
+
# By default, this will load the content of the object.
|
|
68
|
+
# If you don't want this, pass a :lazy option:
|
|
69
|
+
#
|
|
70
|
+
# bucket['key', :lazy]
|
|
71
|
+
def [](key, load_object=true)
|
|
72
|
+
options = {}
|
|
73
|
+
options[:lazy] = true if :lazy == load_object
|
|
74
|
+
object(key, options)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# +options+ are passed to bucket.get; in addition, you can use:
|
|
78
|
+
# * <tt>:lazy => true</tt>: this will prevent S33r from loading the
|
|
79
|
+
# content of the object from S3, instead just getting the object's
|
|
80
|
+
# metadata from the bucket listing.
|
|
81
|
+
def object(key, options={})
|
|
82
|
+
obj = listing[key]
|
|
83
|
+
if obj
|
|
84
|
+
obj.bucket = self
|
|
85
|
+
obj.fetch unless options[:lazy]
|
|
86
|
+
end
|
|
87
|
+
obj
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Can the bucket be used as a log target?
|
|
91
|
+
def log_receiver?
|
|
92
|
+
acl.log_targetable?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Change whether the bucket can be used to receive logs.
|
|
96
|
+
#
|
|
97
|
+
# :on to make it a capable of receiving logs,
|
|
98
|
+
# :off to disable it as a log target.
|
|
99
|
+
def log_receiver(state=:on)
|
|
100
|
+
change_log_target_status(name, state)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/s33r/bucket_listing.rb
CHANGED
|
@@ -1,71 +1,43 @@
|
|
|
1
1
|
base = File.dirname(__FILE__)
|
|
2
2
|
require File.join(base, 'libxml_loader')
|
|
3
|
-
require File.join(base, '
|
|
3
|
+
require File.join(base, 's33r_exception')
|
|
4
4
|
|
|
5
5
|
module S33r
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
# Represents a ListBucketResult
|
|
7
|
+
# (see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/)
|
|
8
|
+
class BucketListing < Hash
|
|
10
9
|
# Name of the bucket this listing is for.
|
|
11
10
|
attr_reader :name
|
|
12
|
-
|
|
13
|
-
attr_reader :contents
|
|
14
|
-
# A NamedBucket instance associated with this listing.
|
|
15
|
-
attr_accessor :named_bucket
|
|
16
|
-
# The last key listed in this BucketListing.
|
|
17
|
-
attr_reader :last_key
|
|
18
|
-
# Set to true to show raw parsing errors, instead of the catch all error message
|
|
19
|
-
# (useful for debugging).
|
|
20
|
-
attr_accessor :raw
|
|
11
|
+
|
|
12
|
+
attr_reader :delimiter, :prefix, :marker, :max_keys, :is_truncated, :common_prefixes, :contents
|
|
21
13
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
# +named_bucket+ can be set to an existing NamedBucket instance, so that any objects
|
|
28
|
-
# inside this listing can be associated with that instance. This enables objects to be easily deleted
|
|
29
|
-
# without having to create a new Client instance.
|
|
30
|
-
#
|
|
31
|
-
# If +raw+ is set to true, you get ugly parser errors.
|
|
32
|
-
def initialize(bucket_listing_xml, named_bucket=nil, raw=false)
|
|
33
|
-
@contents = {}
|
|
34
|
-
@common_prefixes = {}
|
|
35
|
-
# the NamedBucket instance associated with this listing (if any)
|
|
36
|
-
@named_bucket = named_bucket
|
|
37
|
-
@raw = raw
|
|
14
|
+
# +bucket_listing_xml+ is a ListBucketResult document, as returned from a GET on a bucket.
|
|
15
|
+
# +bucket+ is a Bucket instance; any objects in the listing are assigned to this bucket.
|
|
16
|
+
def initialize(bucket_listing_xml)
|
|
17
|
+
@common_prefixes = []
|
|
38
18
|
set_listing_xml(bucket_listing_xml)
|
|
39
19
|
end
|
|
40
20
|
|
|
41
21
|
# Convert a ListBucketResult XML document into an object representation.
|
|
42
22
|
def set_listing_xml(bucket_listing_xml)
|
|
43
|
-
|
|
44
|
-
work = lambda do |bucket_listing_xml|
|
|
45
|
-
# remove the namespace declaration: libxml doesn't like it
|
|
46
|
-
bucket_listing_xml = S33r.remove_namespace(bucket_listing_xml)
|
|
23
|
+
begin
|
|
47
24
|
parse_listing(bucket_listing_xml)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
else
|
|
53
|
-
begin
|
|
54
|
-
work.call(bucket_listing_xml)
|
|
55
|
-
rescue
|
|
56
|
-
message = "Cannot create bucket listing from supplied XML"
|
|
57
|
-
message += " (was nil)" if bucket_listing_xml.nil?
|
|
58
|
-
raise S33rException::InvalidBucketListing, message
|
|
59
|
-
end
|
|
25
|
+
rescue
|
|
26
|
+
message = "Cannot create bucket listing from supplied XML"
|
|
27
|
+
message += " (was nil)" if bucket_listing_xml.nil?
|
|
28
|
+
raise S3Exception::InvalidBucketListing, message
|
|
60
29
|
end
|
|
61
30
|
end
|
|
62
31
|
|
|
63
32
|
# Parse raw XML ListBucketResponse from S3 into object instances.
|
|
64
33
|
# The S3Objects are skeletons, and are not automatically populated
|
|
65
34
|
# from S3 (their @value attributes are nil). To load the data into
|
|
66
|
-
# an object, grab it from the listing and call
|
|
35
|
+
# an object, grab it from the listing and call the fetch method to
|
|
67
36
|
# pull the data down from S3.
|
|
37
|
+
#
|
|
38
|
+
#-- TODO: common_prefixes
|
|
68
39
|
def parse_listing(bucket_listing_xml)
|
|
40
|
+
bucket_listing_xml = S33r.remove_namespace(bucket_listing_xml)
|
|
69
41
|
doc = XML.get_xml_doc(bucket_listing_xml)
|
|
70
42
|
|
|
71
43
|
prop_setter = lambda do |prop, path|
|
|
@@ -84,41 +56,26 @@ module S33r
|
|
|
84
56
|
# contents
|
|
85
57
|
doc.find('//Contents').to_a.each do |node|
|
|
86
58
|
obj = S3Object.from_xml_node(node)
|
|
87
|
-
# Add to the
|
|
88
|
-
|
|
59
|
+
# Add to the contents for the bucket
|
|
60
|
+
self[obj.key] = obj
|
|
89
61
|
end
|
|
90
62
|
end
|
|
91
|
-
|
|
92
|
-
# Get the last key in the contents hash.
|
|
93
|
-
def last_key
|
|
94
|
-
@contents.keys.last
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Return an object in this bucket by key.
|
|
98
|
-
def [](key)
|
|
99
|
-
@contents[key]
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Pretty listing of keys in alphabetical order.
|
|
103
|
-
def pretty
|
|
104
|
-
@contents.keys.sort.each { |k| puts k }
|
|
105
|
-
end
|
|
106
63
|
|
|
107
64
|
# Setters which perform some type casts and normalisation.
|
|
108
65
|
private
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
66
|
+
def name=(val); @name = string_prop_normalise(val); end
|
|
67
|
+
def prefix=(val); @prefix = string_prop_normalise(val); end
|
|
68
|
+
def delimiter=(val); @delimiter = string_prop_normalise(val); end
|
|
69
|
+
def marker=(val); @marker = string_prop_normalise(val); end
|
|
70
|
+
def max_keys=(val); @max_keys = val.to_i; end
|
|
71
|
+
def is_truncated=(val); @is_truncated = ('true' == val || true == val || 'True' == val); end
|
|
115
72
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
73
|
+
# normalise string properties:
|
|
74
|
+
# if value for XML element is nil, set property to empty string
|
|
75
|
+
def string_prop_normalise(val)
|
|
76
|
+
val = '' if val.nil?
|
|
77
|
+
val
|
|
78
|
+
end
|
|
122
79
|
|
|
123
80
|
end
|
|
124
81
|
end
|
data/lib/s33r/client.rb
CHANGED
|
@@ -1,224 +1,163 @@
|
|
|
1
|
-
require 'net/https'
|
|
2
|
-
require 'cgi'
|
|
3
|
-
require 'erb'
|
|
4
|
-
require 'yaml'
|
|
5
1
|
base = File.dirname(__FILE__)
|
|
2
|
+
require File.join(base, 'networking')
|
|
6
3
|
require File.join(base, 's3_acl')
|
|
7
|
-
require File.join(base, '
|
|
4
|
+
require File.join(base, 's3_logging')
|
|
5
|
+
require File.join(base, 'utility')
|
|
8
6
|
|
|
9
7
|
module S33r
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
#
|
|
13
|
-
# using the core to build request headers and content;
|
|
14
|
-
# only client-specific headers are managed here: other headers
|
|
15
|
-
# can be handled by the core.
|
|
16
|
-
#--
|
|
17
|
-
# TODO: use customisable thread pool for requests.
|
|
18
|
-
# TODO: timeout on requests.
|
|
19
|
-
#--
|
|
8
|
+
# Use this class to do operations on the Service, e.g.
|
|
9
|
+
# creating buckets, deleting buckets, listing all buckets,
|
|
10
|
+
# returning a single bucket.
|
|
20
11
|
class Client
|
|
12
|
+
include Networking
|
|
13
|
+
include S3ACL
|
|
14
|
+
include S3Logging
|
|
21
15
|
include S33r
|
|
22
|
-
|
|
23
|
-
# S3 keys.
|
|
24
|
-
attr_accessor :aws_access_key, :aws_secret_access_key
|
|
25
16
|
|
|
26
|
-
#
|
|
27
|
-
|
|
17
|
+
# Options used to create this Client.
|
|
18
|
+
attr_reader :created_with_options
|
|
28
19
|
|
|
29
|
-
|
|
30
|
-
attr_accessor :client_headers
|
|
20
|
+
#-- These are used for creating URLs.
|
|
31
21
|
|
|
32
|
-
#
|
|
22
|
+
# Use SSL for requests.
|
|
33
23
|
attr_accessor :use_ssl
|
|
34
24
|
|
|
35
|
-
#
|
|
36
|
-
attr_accessor :
|
|
25
|
+
# Default expiry for authenticated URLs.
|
|
26
|
+
attr_accessor :expires
|
|
37
27
|
|
|
38
|
-
# Default
|
|
39
|
-
attr_accessor :
|
|
28
|
+
# Default canned ACL string to apply to all put requests.
|
|
29
|
+
attr_accessor :canned_acl
|
|
40
30
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
attr_reader :options
|
|
44
|
-
|
|
45
|
-
# Configure either an SSL-enabled or plain HTTP client.
|
|
46
|
-
# (If using SSL, no verification of server certificate is performed.)
|
|
47
|
-
#
|
|
48
|
-
# +options+: hash of optional client config.:
|
|
49
|
-
# * <tt>:use_ssl => false</tt>: only use plain HTTP for connections
|
|
50
|
-
# * <tt>:dump_requests => true</tt>: dump each request's initial line and headers to STDOUT
|
|
51
|
-
def initialize(aws_access_key, aws_secret_access_key, options={})
|
|
52
|
-
@use_ssl = true
|
|
53
|
-
@use_ssl = false if (false == options[:use_ssl])
|
|
54
|
-
options[:use_ssl] = @use_ssl
|
|
55
|
-
|
|
56
|
-
@dump_requests = (true == options[:dump_requests])
|
|
57
|
-
|
|
58
|
-
# set default chunk size for streaming request body
|
|
59
|
-
@chunk_size = DEFAULT_CHUNK_SIZE
|
|
60
|
-
|
|
61
|
-
@log_bucket = options[:log_bucket]
|
|
62
|
-
|
|
63
|
-
# Amazon S3 developer keys
|
|
64
|
-
@aws_access_key = aws_access_key
|
|
65
|
-
@aws_secret_access_key = aws_secret_access_key
|
|
66
|
-
|
|
67
|
-
# headers sent with every request made by this client
|
|
68
|
-
@client_headers = {}
|
|
69
|
-
|
|
70
|
-
# keep a record of the options used to create this instance
|
|
71
|
-
@options = options
|
|
72
|
-
end
|
|
31
|
+
# Amazon keys.
|
|
32
|
+
attr_accessor :access, :secret
|
|
73
33
|
|
|
74
|
-
# Get
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
client.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
84
|
-
client.use_ssl = true
|
|
85
|
-
else
|
|
86
|
-
client = HTTP.new(HOST, NON_SSL_PORT)
|
|
87
|
-
client.use_ssl = false
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
client
|
|
34
|
+
# Get default options passed to every call to do_request.
|
|
35
|
+
def request_defaults
|
|
36
|
+
defaults = {}
|
|
37
|
+
defaults[:use_ssl] = @use_ssl
|
|
38
|
+
defaults[:expires] = @expires
|
|
39
|
+
defaults[:access] = @access
|
|
40
|
+
defaults[:secret] = @secret
|
|
41
|
+
defaults[:canned_acl] = @canned_acl
|
|
42
|
+
defaults
|
|
91
43
|
end
|
|
92
44
|
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
45
|
+
# Get the settings for this client.
|
|
46
|
+
def settings
|
|
47
|
+
request_defaults.merge(:dump_requests => dump_requests,
|
|
48
|
+
:chunk_size => chunk_size, :persistent => persistent)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Create a plain Client.
|
|
52
|
+
def initialize(options={})
|
|
53
|
+
set_options(options)
|
|
98
54
|
end
|
|
99
55
|
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
56
|
+
# Set options for the client.
|
|
57
|
+
#
|
|
58
|
+
# +options+ may include the following which alter how the Client interacts with S3; they also
|
|
59
|
+
# influence URLs you may generate from the Client:
|
|
60
|
+
# * <tt>:access => 'aws access key'</tt> (defaults to nil)
|
|
61
|
+
# * <tt>:secret => 'aws secret access key'</tt> (defaults to nil)
|
|
62
|
+
# * <tt>:use_ssl => false</tt>: to use plain HTTP for requests
|
|
63
|
+
# sent by this bucket (default=true). If a bucket has :use_ssl => true,
|
|
64
|
+
# any URLs you generate from it will be SSL URLs unless you explicitly
|
|
65
|
+
# disable this behaviour (see url for details).
|
|
66
|
+
# * <tt>:expires => <datetime specifier></tt>: set the default value to be passed as the :expires
|
|
67
|
+
# option when generating authenticated URLs. Should be parseable by S33r.parse_expiry.
|
|
68
|
+
# * <tt>:canned_acl => 'public-read'</tt>: set a default canned acl to apply to all put
|
|
69
|
+
# requests.
|
|
70
|
+
#
|
|
71
|
+
# These options change the behaviour of the HTTP client which actually sends the request:
|
|
72
|
+
# * <tt>:chunk_size => Integer</tt>: use a non-standard chunk size;
|
|
73
|
+
# default is to use S33r::DEFAULT_CHUNK_SIZE.
|
|
74
|
+
# * <tt>:persistent => true</tt>: use persistent HTTP connections
|
|
75
|
+
# (default=false).
|
|
76
|
+
# * <tt>:dump_requests => true</tt>: to dump all request headers before the request is sent.
|
|
77
|
+
def set_options(options={})
|
|
78
|
+
# General client options.
|
|
79
|
+
@access = options[:access]
|
|
80
|
+
@secret = options[:secret]
|
|
81
|
+
@use_ssl = true
|
|
82
|
+
@use_ssl = false if (false == options[:use_ssl])
|
|
83
|
+
@expires = options[:expires] || 'never'
|
|
84
|
+
@canned_acl = options[:canned_acl] || nil
|
|
117
85
|
|
|
118
|
-
|
|
119
|
-
|
|
86
|
+
# Options specific to the mechanics of the HTTP request.
|
|
87
|
+
@dump_requests = options[:dump_requests] || false
|
|
88
|
+
@chunk_size = options[:chunk_size]
|
|
89
|
+
@persistent = options[:persistent] || false
|
|
120
90
|
|
|
121
|
-
|
|
91
|
+
@created_with_options = options
|
|
122
92
|
end
|
|
123
|
-
|
|
124
|
-
# Send a request over the wire.
|
|
125
|
-
#
|
|
126
|
-
# This method streams +data+ if it responds to the +stat+ method
|
|
127
|
-
# (as files do).
|
|
128
|
-
#
|
|
129
|
-
# Returns a Net::HTTPResponse instance.
|
|
130
|
-
def do_request(method, path, data=nil, headers={})
|
|
131
|
-
req = get_requester(method, path)
|
|
132
|
-
req.chunk_size = @chunk_size
|
|
133
|
-
|
|
134
|
-
# Add the S3 headers which are always required.
|
|
135
|
-
headers = add_default_headers(headers)
|
|
136
|
-
|
|
137
|
-
# Add any client-specific default headers.
|
|
138
|
-
headers = add_client_headers(headers)
|
|
139
|
-
|
|
140
|
-
# Generate the S3 authorization header.
|
|
141
|
-
headers['Authorization'] = generate_auth_header_value(method, path, headers,
|
|
142
|
-
@aws_access_key, @aws_secret_access_key)
|
|
143
|
-
|
|
144
|
-
# Insert the headers into the request object.
|
|
145
|
-
headers.each do |key, value|
|
|
146
|
-
req[key] = value
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Add data to the request as a stream.
|
|
150
|
-
if req.request_body_permitted?
|
|
151
|
-
# For streaming files; NB Content-Length will be set by Net::HTTP
|
|
152
|
-
# for character-based data: this section of code is only used
|
|
153
|
-
# when reading directly from a file.
|
|
154
|
-
if data.respond_to?(:stat)
|
|
155
|
-
req.body_stream = data
|
|
156
|
-
req['Content-Length'] = data.stat.size.to_s
|
|
157
|
-
data = nil
|
|
158
|
-
end
|
|
159
|
-
else
|
|
160
|
-
data = nil
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
if @dump_requests
|
|
164
|
-
puts req.to_s
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Run the request.
|
|
168
|
-
client = get_client
|
|
169
|
-
client.start do
|
|
170
|
-
response = client.request(req, data)
|
|
171
|
-
|
|
172
|
-
# Check the response to see whether S3 is down;
|
|
173
|
-
# raises an S3FallenOver error if S3 returns a 500-503 response code
|
|
174
|
-
response.check_s3_availability
|
|
175
|
-
|
|
176
|
-
response
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Return an instance of an appropriate request class.
|
|
181
|
-
def get_requester(method, path)
|
|
182
|
-
raise S33rException::UnsupportedHTTPMethod, "The #{method} HTTP method is not supported" if !(METHOD_VERBS.include?(method))
|
|
183
|
-
eval("HTTP::" + method[0,1].upcase + method[1..-1].downcase + ".new('#{path}')")
|
|
184
|
-
end
|
|
185
|
-
|
|
93
|
+
|
|
186
94
|
# List all buckets.
|
|
187
95
|
#
|
|
188
|
-
# Returns an array of
|
|
96
|
+
# Returns an array of Bucket instances; array will be empty if
|
|
189
97
|
# the BucketListing parse fails for any reason (i.e. no <Bucket> elements
|
|
190
98
|
# occur in it).
|
|
191
|
-
|
|
192
|
-
|
|
99
|
+
#
|
|
100
|
+
# +options+ is passed through to get_bucket, making it possible to detach
|
|
101
|
+
# retrieved buckets from the Client instance, and to pass other options to
|
|
102
|
+
# the bucket.
|
|
103
|
+
def buckets(options={})
|
|
104
|
+
resp = do_get
|
|
105
|
+
|
|
106
|
+
bucket_list_xml = resp.body
|
|
193
107
|
doc = XML.get_xml_doc(S33r.remove_namespace(bucket_list_xml))
|
|
194
108
|
|
|
195
|
-
|
|
109
|
+
buckets = {}
|
|
196
110
|
|
|
197
111
|
doc.find("//Bucket").to_a.each do |node|
|
|
198
112
|
bucket_name = node.xget('Name')
|
|
199
113
|
if bucket_name
|
|
200
|
-
#
|
|
114
|
+
# CreationDate is a string in format '2006-10-17T15:14:39.000Z'.
|
|
115
|
+
creation_date = Time.parse(node.xget('CreationDate'))
|
|
116
|
+
# The Bucket instances inherit the request dumping behaviour
|
|
201
117
|
# of this client.
|
|
202
|
-
|
|
203
|
-
{:default_bucket => bucket_name, :dump_request => self.dump_requests})
|
|
118
|
+
buckets[bucket_name] = get_bucket(bucket_name, options)
|
|
204
119
|
end
|
|
205
120
|
end
|
|
206
121
|
|
|
207
|
-
|
|
122
|
+
buckets
|
|
208
123
|
end
|
|
124
|
+
alias :list_buckets :buckets
|
|
209
125
|
|
|
210
|
-
#
|
|
211
|
-
def
|
|
212
|
-
|
|
126
|
+
# Just get a sorted array of names of buckets.
|
|
127
|
+
def bucket_names
|
|
128
|
+
buckets.keys.sort
|
|
213
129
|
end
|
|
214
|
-
|
|
130
|
+
|
|
131
|
+
# Get a Client instance bound to a bucket.
|
|
132
|
+
#
|
|
133
|
+
# +options+:
|
|
134
|
+
# * <tt>:orphan => true</tt>: create the Client in isolation from
|
|
135
|
+
# the Service and don't inherit any of its instance settings.
|
|
136
|
+
#
|
|
137
|
+
# Other options are passed through to Bucket.new.
|
|
138
|
+
def get_bucket(bucket_name, options={})
|
|
139
|
+
orphan = options.delete(:orphan)
|
|
140
|
+
unless orphan
|
|
141
|
+
options.merge!(settings) { |key, old_val, new_val| old_val }
|
|
142
|
+
end
|
|
143
|
+
bucket = Bucket.new(bucket_name, options)
|
|
144
|
+
yield bucket if block_given?
|
|
145
|
+
bucket
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Create a new Bucket.
|
|
149
|
+
def create_bucket(name, options={})
|
|
150
|
+
options[:create] = true
|
|
151
|
+
get_bucket(name, options)
|
|
152
|
+
end
|
|
153
|
+
|
|
215
154
|
# List entries in a bucket.
|
|
216
155
|
#
|
|
217
|
-
# +
|
|
156
|
+
# +options+: hash of options on the bucket listing request, passed as querystring parameters to S3
|
|
218
157
|
# (see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/).
|
|
219
158
|
# * <tt>:prefix => 'some_string'</tt>: restrict results to keys beginning with 'some_string'
|
|
220
159
|
# * <tt>:marker => 'some_string'</tt>: restict results to keys occurring lexicographically after 'some_string'
|
|
221
|
-
# * <tt>:max_keys =>
|
|
160
|
+
# * <tt>:max_keys => Integer</tt>: return at most this number of keys (maximum possible value is 1000)
|
|
222
161
|
# * <tt>:delimiter => 'some_string'</tt>: keys containing the same string between prefix and the delimiter
|
|
223
162
|
# are rolled up into a CommonPrefixes element inside the response
|
|
224
163
|
#
|
|
@@ -227,319 +166,239 @@ module S33r
|
|
|
227
166
|
#
|
|
228
167
|
# To page through a bucket 10 keys at a time, you can do:
|
|
229
168
|
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
169
|
+
# listing = list_bucket('mybucket', :max_keys => 10)
|
|
170
|
+
# listing = list_bucket('mybucket', :max_keys => 11, :marker => listing.last_key)
|
|
171
|
+
# listing = list_bucket('mybucket', :max_keys => 11, :marker => listing.last_key)
|
|
233
172
|
# etc.
|
|
234
173
|
#
|
|
235
174
|
# Note in the example code, +listing+ is a BucketListing instance; call its contents method
|
|
236
175
|
# to get a hash of the keys in the bucket, along with associated objects.
|
|
237
176
|
#
|
|
238
|
-
# Returns
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
177
|
+
# Returns BucketListing instance.
|
|
178
|
+
#-- TODO: testing
|
|
179
|
+
def listing(options={})
|
|
180
|
+
querystring = options[:querystring] || {}
|
|
181
|
+
|
|
182
|
+
# Check :max_keys isn't higher than the maximum allowed by S3.
|
|
183
|
+
if options[:max_keys]
|
|
184
|
+
max_keys = options[:max_keys].to_i
|
|
243
185
|
if max_keys > BUCKET_LIST_MAX_MAX_KEYS
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
186
|
+
raise S3Exception::BucketListingMaxKeysError, "max_keys option to list bucket cannot be > #{BUCKET_LIST_MAX_MAX_KEYS}"
|
|
187
|
+
end
|
|
188
|
+
querystring['max-keys'] = max_keys
|
|
247
189
|
end
|
|
248
190
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
# Create a bucket.
|
|
255
|
-
#
|
|
256
|
-
# Returns true if response returned a 200 code; false otherwise.
|
|
257
|
-
def create_bucket(bucket_name, headers={})
|
|
258
|
-
resp = do_put("/#{bucket_name}", nil, headers)
|
|
259
|
-
resp.ok?
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
# Delete a bucket.
|
|
263
|
-
#
|
|
264
|
-
# +options+ hash can contain the following:
|
|
265
|
-
# * <tt>:force => true</tt>: delete all keys within the bucket then delete the bucket itself
|
|
266
|
-
#-- TODO: maybe delete keys matching a partial path
|
|
267
|
-
#-- TODO: if multiple pages of keys in buckets, need to get them by page.
|
|
268
|
-
def delete_bucket(bucket_name, headers={}, options={})
|
|
269
|
-
if true == options[:force]
|
|
270
|
-
_, bucket_listing = list_bucket(bucket_name)
|
|
271
|
-
bucket_listing.contents.each_value do |obj|
|
|
272
|
-
delete_resource(bucket_name, obj.key)
|
|
273
|
-
end
|
|
191
|
+
['prefix', 'marker', 'delimiter'].each do |key|
|
|
192
|
+
key_sym = key.to_sym
|
|
193
|
+
querystring[key] = options[key_sym] if options[key_sym]
|
|
274
194
|
end
|
|
275
195
|
|
|
276
|
-
|
|
196
|
+
options[:querystring] = querystring
|
|
197
|
+
|
|
198
|
+
resp = do_get(options)
|
|
199
|
+
|
|
200
|
+
if resp.ok?
|
|
201
|
+
@listing = BucketListing.new(resp.body)
|
|
202
|
+
else
|
|
203
|
+
raise resp.s3_error
|
|
204
|
+
end
|
|
277
205
|
end
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
206
|
+
alias :objects :listing
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# List content of a bucket.
|
|
210
|
+
def list_bucket(bucket_name, options={})
|
|
211
|
+
options[:bucket] = bucket_name
|
|
212
|
+
listing(options)
|
|
282
213
|
end
|
|
283
214
|
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
# +options
|
|
287
|
-
# the
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
yield named_bucket if block_given?
|
|
295
|
-
named_bucket
|
|
215
|
+
# Delete a Bucket.
|
|
216
|
+
#
|
|
217
|
+
# +options+:
|
|
218
|
+
# * <tt>:force => true</tt>: To clear the content of the bucket first.
|
|
219
|
+
def delete_bucket(bucket_name, options={})
|
|
220
|
+
options[:bucket] = bucket_name
|
|
221
|
+
if options[:force]
|
|
222
|
+
listing(options).keys.each { |key| do_delete(options.merge(:key => key)) }
|
|
223
|
+
end
|
|
224
|
+
do_delete(options).ok?
|
|
296
225
|
end
|
|
297
226
|
|
|
298
|
-
#
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def get_resource(bucket_name, resource_key, headers={})
|
|
303
|
-
do_get("/#{bucket_name}/#{resource_key}", headers)
|
|
227
|
+
# Check whether a bucket exists on S3.
|
|
228
|
+
def bucket_exists?(name, options={})
|
|
229
|
+
options[:bucket] = name
|
|
230
|
+
do_head(options).ok?
|
|
304
231
|
end
|
|
305
232
|
|
|
306
|
-
#
|
|
307
|
-
#
|
|
308
|
-
#
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
233
|
+
# Put a "thing" onto S3.
|
|
234
|
+
#
|
|
235
|
+
# +thing+ may be a string, an S3Object, an S3ACL::Policy,
|
|
236
|
+
# a LoggingResource or a file handle.
|
|
237
|
+
#
|
|
238
|
+
# Anything you pass in +options+ will override any values
|
|
239
|
+
# inferred from the +thing+ (e.g. content type, key).
|
|
240
|
+
#
|
|
241
|
+
# +options+:
|
|
242
|
+
# * <tt>:key => 'some-key'</tt> (required unless thing is an S3Object).
|
|
243
|
+
# * <tt>:bucket => 'some-bucket'</tt>
|
|
244
|
+
# * <tt>:content_type => 'text/plain'</tt>
|
|
245
|
+
# * <tt>:render_as_attachment => Boolean</tt>
|
|
246
|
+
# * <tt>:file => true</tt>: thing is a filename, so load it as a file
|
|
247
|
+
# * <tt>:canned_acl => 'public'</tt>: one of S33r::CANNED_ACLS, to set a canned
|
|
248
|
+
# acl on a put.
|
|
249
|
+
#
|
|
250
|
+
#-- TODO: finish documentation for options
|
|
251
|
+
#-- TODO: implement canned_acl
|
|
252
|
+
#-- TODO: pass Policy as an option
|
|
253
|
+
def put(thing, options={}, headers={})
|
|
254
|
+
is_file = options[:file]
|
|
255
|
+
|
|
256
|
+
# thing is a file, so load it.
|
|
257
|
+
if is_file and thing.is_a?(String)
|
|
258
|
+
# Use the filename as the key unless it is set already.
|
|
259
|
+
options[:key] ||= thing
|
|
260
|
+
|
|
261
|
+
# Guess the content type unless it's been set.
|
|
262
|
+
unless options[:content_type]
|
|
263
|
+
mime_type = guess_mime_type(thing)
|
|
264
|
+
content_type = mime_type.simplified
|
|
265
|
+
options[:content_type] = content_type
|
|
266
|
+
end
|
|
267
|
+
elsif thing.is_a?(S3Object)
|
|
268
|
+
options[:key] ||= thing.key
|
|
269
|
+
data = thing.value
|
|
270
|
+
options[:content_type] ||= thing.content_type
|
|
271
|
+
options[:render_as_attachment] ||= thing.render_as_attachment
|
|
272
|
+
headers = metadata_headers(thing.meta)
|
|
273
|
+
elsif thing.is_a?(Policy) || thing.is_a?(LoggingResource)
|
|
274
|
+
data = thing.to_xml
|
|
275
|
+
options[:content_type] = 'text/xml'
|
|
276
|
+
else
|
|
277
|
+
data = thing
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
key = options[:key]
|
|
281
|
+
|
|
282
|
+
# Headers for content type etc.
|
|
283
|
+
headers.merge! content_headers(options[:content_type], key, options[:render_as_attachment])
|
|
284
|
+
|
|
285
|
+
if is_file
|
|
286
|
+
File.open(thing) do |data|
|
|
287
|
+
do_put(data, options, headers).ok?
|
|
288
|
+
end
|
|
289
|
+
else
|
|
290
|
+
do_put(data, options, headers).ok?
|
|
291
|
+
end
|
|
313
292
|
end
|
|
314
293
|
|
|
315
|
-
#
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
S3Object.from_response(resource_key, response)
|
|
294
|
+
# Put a file onto S3 (shortcut to put).
|
|
295
|
+
def put_file(filename, options={}, headers={})
|
|
296
|
+
options[:file] = true
|
|
297
|
+
put(filename, options, headers)
|
|
320
298
|
end
|
|
321
299
|
|
|
322
|
-
#
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
response = do_get(path)
|
|
329
|
-
if response.ok?
|
|
330
|
-
S3ACL::ACLDoc.from_xml(response.body)
|
|
300
|
+
# Get an ACL.
|
|
301
|
+
def get_acl(options={})
|
|
302
|
+
options[:acl] = true
|
|
303
|
+
resp = do_get(options)
|
|
304
|
+
if resp.ok?
|
|
305
|
+
S3ACL::Policy.from_xml(resp.body)
|
|
331
306
|
else
|
|
332
|
-
|
|
307
|
+
nil
|
|
333
308
|
end
|
|
334
309
|
end
|
|
310
|
+
alias :acl :get_acl
|
|
335
311
|
|
|
336
|
-
#
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
# Returns true if response had a 200 code, false otherwise.
|
|
341
|
-
# If you get a 400 Bad Request back, it means a CanonicalUser
|
|
342
|
-
# could not be identified from the email address.
|
|
343
|
-
def set_acl(acl_doc, bucket_name, resource_key='')
|
|
344
|
-
path = s3_acl_path(bucket_name, resource_key)
|
|
345
|
-
response = do_put(path, acl_doc.to_xml)
|
|
346
|
-
response.ok?
|
|
312
|
+
# Set an ACL.
|
|
313
|
+
def set_acl(policy, options={})
|
|
314
|
+
options[:acl] = true
|
|
315
|
+
put(policy, options)
|
|
347
316
|
end
|
|
317
|
+
alias :acl= :set_acl
|
|
348
318
|
|
|
349
|
-
#
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
# +bucket_name+ = a bucket to log.
|
|
353
|
-
# +resource_key+ = a resource to log (if empty, logging
|
|
354
|
-
# gets added to the bucket).
|
|
355
|
-
def set_logging(logging_resource, bucket_name, resource_key='')
|
|
356
|
-
path = s3_logging_path(bucket_name, resource_key)
|
|
357
|
-
response = do_put(path, logging_resource.to_xml)
|
|
319
|
+
# Is a resource public?
|
|
320
|
+
def public?(options={})
|
|
321
|
+
get_acl(options).public_readable?
|
|
358
322
|
end
|
|
359
323
|
|
|
360
|
-
# Make a resource public
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
#
|
|
364
|
-
# Returns nil if resource does not exist.
|
|
365
|
-
def make_public(bucket_name, resource_key='')
|
|
366
|
-
acl = get_acl(bucket_name, resource_key)
|
|
367
|
-
if !acl.nil? and acl.add_public_read_grants
|
|
368
|
-
set_acl(acl, bucket_name, resource_key)
|
|
369
|
-
end
|
|
324
|
+
# Make a resource public
|
|
325
|
+
def make_public
|
|
326
|
+
set_acl(get_acl().add_public_read_grant)
|
|
370
327
|
end
|
|
371
328
|
|
|
372
|
-
|
|
329
|
+
# Make a resource private
|
|
373
330
|
def make_private
|
|
331
|
+
set_acl(get_acl().remove_public_read_grant)
|
|
374
332
|
end
|
|
375
333
|
|
|
376
|
-
#
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
#
|
|
381
|
-
#-- TODO: tests
|
|
382
|
-
def enable_log_target(bucket_name)
|
|
383
|
-
acl = get_acl(bucket_name)
|
|
384
|
-
if acl.add_log_target_grants
|
|
385
|
-
set_acl(acl, bucket_name)
|
|
386
|
-
end
|
|
387
|
-
acl.log_targetable?
|
|
334
|
+
# Get a URL for a thing.
|
|
335
|
+
def url(options={})
|
|
336
|
+
options = request_defaults.merge(options)
|
|
337
|
+
s3_url(options)
|
|
388
338
|
end
|
|
389
339
|
|
|
390
|
-
#
|
|
391
|
-
#
|
|
392
|
-
#
|
|
393
|
-
#
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
340
|
+
# Change the status of a bucket for logging.
|
|
341
|
+
#
|
|
342
|
+
# +logging_on+ = :on to turn logging on (default),
|
|
343
|
+
# :off to turn logging off.
|
|
344
|
+
def change_log_target_status(bucket_name, state=:on)
|
|
345
|
+
logging_on = (:on == state)
|
|
346
|
+
bucket = get_bucket(bucket_name)
|
|
347
|
+
policy = bucket.acl
|
|
348
|
+
logging_on ? policy.add_log_target_grants : policy.remove_log_target_grants
|
|
349
|
+
bucket.acl = policy
|
|
350
|
+
logging_on == policy.log_targetable?
|
|
401
351
|
end
|
|
402
352
|
|
|
403
|
-
#
|
|
404
|
-
#
|
|
405
|
-
# +
|
|
406
|
-
#
|
|
407
|
-
#
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
def enable_logging(bucket_name, log_bucket=nil, options={})
|
|
417
|
-
# Set to the default log_bucket if not set explicitly.
|
|
418
|
-
log_bucket ||= @log_bucket
|
|
419
|
-
|
|
420
|
-
resource_key = options[:for_key]
|
|
421
|
-
resource_key ||= ''
|
|
422
|
-
|
|
423
|
-
log_prefix = options[:prefix]
|
|
424
|
-
log_prefix ||= bucket_name + '-'
|
|
425
|
-
|
|
426
|
-
log_bucket_acl = get_acl(log_bucket)
|
|
427
|
-
if !(log_bucket_acl.log_targetable?)
|
|
428
|
-
raise BucketNotLogTargetable, "The bucket #{log_bucket} cannot be specified as a log target"
|
|
353
|
+
# Get the logging status for a resource.
|
|
354
|
+
#
|
|
355
|
+
# +options+:
|
|
356
|
+
# * <tt>:for_bucket => 'bucket'</tt>: get the logging status for a bucket.
|
|
357
|
+
# (Alias for :bucket; if both supplied, :bucket takes preference.)
|
|
358
|
+
def logging(options={})
|
|
359
|
+
options[:logging] = true
|
|
360
|
+
options[:bucket] ||= options[:for_bucket]
|
|
361
|
+
resp = do_get(options)
|
|
362
|
+
if resp.ok?
|
|
363
|
+
LoggingResource.from_xml(resp.body)
|
|
364
|
+
else
|
|
365
|
+
nil
|
|
429
366
|
end
|
|
430
|
-
logging_resource = LoggingResource.new(log_bucket, log_prefix)
|
|
431
|
-
set_logging(logging_resource, bucket_name, resource_key)
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
# Turn off logging of a resource.
|
|
435
|
-
#-- TODO
|
|
436
|
-
def disable_logging
|
|
437
367
|
end
|
|
438
368
|
|
|
439
|
-
#
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
#
|
|
445
|
-
#
|
|
446
|
-
#
|
|
447
|
-
#
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
# Put a string onto S3.
|
|
455
|
-
def put_text(string, bucket_name, resource_key, headers={})
|
|
456
|
-
headers["Content-Type"] = "text/plain"
|
|
457
|
-
put_resource(bucket_name, resource_key, string, headers)
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
# Put a file onto S3.
|
|
461
|
-
#
|
|
462
|
-
# If +resource_key+ is nil, the filename is used as the key instead.
|
|
463
|
-
#
|
|
464
|
-
# +headers+ sets some headers with the request; useful if you have an odd file type
|
|
465
|
-
# not recognised by the mimetypes library, and want to explicitly set the Content-Type header.
|
|
466
|
-
#
|
|
467
|
-
# +options+ hash simplifies setting some headers with specific meaning to S3:
|
|
468
|
-
# * <tt>:render_as_attachment => true</tt>: set the Content-Disposition for this file to "attachment" and set
|
|
469
|
-
# the default filename for saving the file (when accessed by a web browser) to +filename+; this
|
|
470
|
-
# turns the file into a download when opened in a browser, rather than trying to render it inline.
|
|
471
|
-
#
|
|
472
|
-
# Note that this method uses a handle to the file, so it can be streamed in chunks to S3.
|
|
473
|
-
def put_file(filename, bucket_name, resource_key=nil, headers={}, options={})
|
|
474
|
-
# default to the file path as the resource key if none explicitly set
|
|
475
|
-
if resource_key.nil?
|
|
476
|
-
resource_key = filename
|
|
477
|
-
end
|
|
369
|
+
# Enable logging for a bucket.
|
|
370
|
+
#
|
|
371
|
+
# +target_bucket+ is the target for the logs.
|
|
372
|
+
# The bucket you want to log is passed in +options+.
|
|
373
|
+
#
|
|
374
|
+
# +options+
|
|
375
|
+
# * <tt>:bucket => 'bucket'</tt>: bucket to log.
|
|
376
|
+
# * <tt>:for_bucket => 'bucket'</tt>: syntactic sugar; alias for <tt>:bucket</tt>.
|
|
377
|
+
# If :bucket and :for_bucket are provided, :bucket takes preference.
|
|
378
|
+
# * <tt>:prefix => 'some-prefix-'</tt>: specify a prefix for log files;
|
|
379
|
+
# otherwise 'log-<bucket name>-' is used
|
|
380
|
+
def logs_to(target_bucket, options={})
|
|
381
|
+
target_bucket_name = target_bucket.is_a?(Bucket) ? target_bucket.name : target_bucket
|
|
382
|
+
log_prefix = options[:prefix] || "log-#{bucket_name}-"
|
|
383
|
+
options[:bucket] ||= options[:for_bucket]
|
|
478
384
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
# content type is explicitly set in the headers, so apply to request
|
|
485
|
-
if headers[:content_type]
|
|
486
|
-
# use the first MIME type corresponding to this content type string
|
|
487
|
-
# (MIME::Types returns an array of possible MIME types)
|
|
488
|
-
mime_type = MIME::Types[headers[:content_type]][0]
|
|
489
|
-
else
|
|
490
|
-
# we're not going to use this much, just for parsing the content type etc.
|
|
491
|
-
mime_type = guess_mime_type(filename)
|
|
492
|
-
end
|
|
493
|
-
content_type = mime_type.simplified
|
|
494
|
-
headers['Content-Type'] = content_type
|
|
495
|
-
headers['Content-Transfer-Encoding'] = 'binary' if mime_type.binary?
|
|
496
|
-
|
|
497
|
-
# Open the file, and pass the handle to the HTTP client so content
|
|
498
|
-
# can be streamed.
|
|
499
|
-
File.open(filename) do |data|
|
|
500
|
-
# send the put request
|
|
501
|
-
put_resource(bucket_name, resource_key, data, headers)
|
|
385
|
+
target_bucket_acl = get_acl(:bucket => target_bucket_name)
|
|
386
|
+
unless target_bucket_acl.log_targetable?
|
|
387
|
+
raise BucketNotLogTargetable, "The bucket #{target_bucket_name} cannot be specified as a log target"
|
|
502
388
|
end
|
|
389
|
+
|
|
390
|
+
logging_resource = LoggingResource.new(target_bucket_name, log_prefix)
|
|
391
|
+
options[:logging] = true
|
|
392
|
+
put(logging_resource, options)
|
|
503
393
|
end
|
|
504
394
|
|
|
505
|
-
#
|
|
395
|
+
# Turn off logging for a bucket.
|
|
506
396
|
#
|
|
507
|
-
#
|
|
508
|
-
#
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
# Add any default headers which should be sent with every request from the client.
|
|
515
|
-
#
|
|
516
|
-
# +headers+ is a hash of headers already set up. Any headers passed in here
|
|
517
|
-
# override the defaults in +client_headers+.
|
|
518
|
-
#
|
|
519
|
-
# Returns +headers+ with the content of +client_headers+ merged in.
|
|
520
|
-
def add_client_headers(headers)
|
|
521
|
-
headers.merge!(client_headers) { |key, arg, default| arg }
|
|
522
|
-
end
|
|
523
|
-
|
|
524
|
-
def do_get(path='/', headers={})
|
|
525
|
-
do_request('GET', path, nil, headers)
|
|
397
|
+
# +options+:
|
|
398
|
+
# * <tt>:bucket => 'bucket'</tt>: bucket to turn logging off for.
|
|
399
|
+
def logs_off(options={})
|
|
400
|
+
options[:logging] = true
|
|
401
|
+
put(LoggingResource.new, options)
|
|
526
402
|
end
|
|
527
|
-
|
|
528
|
-
def do_head(path='/', headers={})
|
|
529
|
-
do_request('HEAD', path, nil, headers)
|
|
530
|
-
end
|
|
531
|
-
|
|
532
|
-
def do_post(path='/', data=nil, headers={})
|
|
533
|
-
do_request('POST', path, data, headers)
|
|
534
|
-
end
|
|
535
|
-
|
|
536
|
-
def do_put(path='/', data=nil, headers={})
|
|
537
|
-
do_request('PUT', path, data, headers)
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
def do_delete(path, headers={})
|
|
541
|
-
do_request('DELETE', path, nil, headers)
|
|
542
|
-
end
|
|
543
|
-
|
|
544
403
|
end
|
|
545
|
-
end
|
|
404
|
+
end
|