qoobaa-stree 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ *.gem
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Jakub Kuźma, Mirosław Boruta
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.
data/README.rdoc ADDED
@@ -0,0 +1,48 @@
1
+ = Stree
2
+
3
+ * repository: http://github.com/qoobaa/stree
4
+ * issue tracker: http://github.com/qoobaa/stree/issues
5
+ * rdoc: http://qoobaa.github.com/stree
6
+
7
+ == Usage
8
+
9
+ Coming soon, see Rdoc documentation.
10
+
11
+ == Command tool usage
12
+
13
+ You have to pass access key id (-a) and secret access key (-s) to the
14
+ command line tool. Stree reads ACCESS_KEY_ID and SECRET_ACCESS_KEY
15
+ environment variables and uses them by default, so if you don't want
16
+ to pass them each time, export them (e.g. in .bashrc file).
17
+
18
+ * list buckets
19
+ stree bucket
20
+
21
+ * create bucket
22
+ stree bucket add name-of-bucket
23
+
24
+ * list objects in bucket
25
+ stree bucket show name-of-bucket
26
+
27
+ * destroy bucket
28
+ stree bucket remove name-of-bucket
29
+
30
+ * list objects (in all buckets)
31
+ stree object
32
+
33
+ * show the object information
34
+ stree object show bucket_name/path/to/object.extension
35
+
36
+ * download the content of the object
37
+ stree object show bucket_name/path/to/object.extension filename_to_store_the_content.extension
38
+
39
+ * show the content of the object to STDOUT
40
+ stree object show bucket_name/path/to/object.extension -
41
+
42
+ You can also pass --help to the commmands like:
43
+ stree bucket add --help
44
+ stree object show --help
45
+
46
+ == Copyright
47
+
48
+ Copyright (c) 2009 Jakub Kuźma, Mirosław Boruta. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,58 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = "stree"
10
+ gem.summary = %Q{Library for accessing S3 objects and buckets, with command line tool}
11
+ gem.email = "qoobaa@gmail.com"
12
+ gem.homepage = "http://github.com/qoobaa/stree"
13
+ gem.authors = ["Jakub Kuźma", "Mirosław Boruta"]
14
+ gem.add_dependency "trollop", ">=1.14"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
20
+ end
21
+
22
+ require 'rake/testtask'
23
+ Rake::TestTask.new(:test) do |test|
24
+ test.libs << 'lib' << 'test'
25
+ test.pattern = 'test/**/*_test.rb'
26
+ test.verbose = true
27
+ end
28
+
29
+ begin
30
+ require 'rcov/rcovtask'
31
+ Rcov::RcovTask.new do |test|
32
+ test.libs << 'test'
33
+ test.pattern = 'test/**/*_test.rb'
34
+ test.verbose = true
35
+ end
36
+ rescue LoadError
37
+ task :rcov do
38
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
39
+ end
40
+ end
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ if File.exist?('VERSION.yml')
47
+ config = YAML.load(File.read('VERSION.yml'))
48
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
49
+ else
50
+ version = ""
51
+ end
52
+
53
+ rdoc.rdoc_dir = 'rdoc'
54
+ rdoc.title = "stree #{version}"
55
+ rdoc.rdoc_files.include('README*')
56
+ rdoc.rdoc_files.include('lib/**/*.rb')
57
+ end
58
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/bin/stree ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.expand_path(File.dirname(__FILE__) + "/../lib")
4
+
5
+ require "trollop"
6
+ require "stree"
7
+
8
+ # HELPER METHODS
9
+
10
+ include Stree
11
+
12
+ def list_buckets(service)
13
+ service.buckets.each do |bucket|
14
+ puts bucket.name
15
+ end
16
+ end
17
+
18
+ def create_bucket(service, name, location)
19
+ service.buckets.build(name).save(location)
20
+ end
21
+
22
+ def destroy_bucket(service, name)
23
+ service.buckets.find(name).destroy
24
+ end
25
+
26
+ def show_bucket(service, name, options = {})
27
+ service.buckets.find(name).objects.find_all.each do |object|
28
+ puts "#{name}/#{object.key}"
29
+ end
30
+ end
31
+
32
+ def list_objects(service)
33
+ service.buckets.each do |bucket|
34
+ bucket.objects.each do |object|
35
+ puts "#{bucket.name}/#{object.key}"
36
+ end
37
+ end
38
+ end
39
+
40
+ def create_object(service, name, file_name, options = {})
41
+ bucket_name, object_name = name.split("/", 2)
42
+ object = service.buckets.find(bucket_name).objects.build(object_name)
43
+ object.content_type = options[:type]
44
+ object.content_encoding = options[:encoding]
45
+ object.content_disposition = options[:disposition]
46
+ object.acl = options[:acl]
47
+ object.content = File.new(file_name)
48
+ object.save
49
+ end
50
+
51
+ def destroy_object(service, name)
52
+ bucket_name, object_name = name.split("/", 2)
53
+ object = service.buckets.find(bucket_name).objects.find(object_name)
54
+ object.destroy
55
+ end
56
+
57
+ def show_object(service, name, file_name = nil)
58
+ bucket_name, object_name = name.split("/", 2)
59
+ object = service.buckets.find(bucket_name).objects.find_first(object_name)
60
+ puts " object: #{object.name}/#{object.key}"
61
+ puts " content type: #{object.content_type}"
62
+ puts " size: #{object.size}"
63
+ puts " etag: #{object.etag}"
64
+ puts " last modified: #{object.last_modified}"
65
+ if file_name
66
+ if file_name == "-"
67
+ puts object.content
68
+ else
69
+ File.open(file_name, "wb") do |file|
70
+ file.write(object.content)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ # COMMAND LINE PARSER
77
+
78
+ ACCESS_KEY_ID = ENV["ACCESS_KEY_ID"]
79
+ SECRET_ACCESS_KEY = ENV["SECRET_ACCESS_KEY"]
80
+ COMMANDS = %w(bucket object)
81
+ BUCKET_SUBCOMMANDS = %w(add remove show)
82
+ OBJECT_SUBCOMMANDS = %w(add remove show)
83
+
84
+ global_options = Trollop::options do
85
+ banner "stree - s3 command line tool"
86
+ opt :access_key_id, "Your access key id to AWS", :type => :string, :default => ACCESS_KEY_ID
87
+ opt :secret_access_key, "Your secret access key to AWS", :type => :string, :default => SECRET_ACCESS_KEY
88
+ opt :debug, "Debug mode", :type => :flag, :default => false
89
+ stop_on COMMANDS
90
+ end
91
+
92
+ Trollop::die "No access key id given" unless global_options[:access_key_id]
93
+ Trollop::die "No secret access key given" unless global_options[:secret_access_key]
94
+
95
+ service = Service.new(:access_key_id => global_options[:access_key_id],
96
+ :secret_access_key => global_options[:secret_access_key],
97
+ :debug => global_options[:debug])
98
+
99
+ command = ARGV.shift
100
+
101
+ begin
102
+ case command
103
+ when "bucket"
104
+ command_options = Trollop::options do
105
+ banner "manage buckets"
106
+ stop_on BUCKET_SUBCOMMANDS
107
+ end
108
+ subcommand = ARGV.shift
109
+ case subcommand
110
+ when "add"
111
+ subcommand_options = Trollop::options do
112
+ banner "add bucket"
113
+ opt :location, "Location of the bucket - EU or US", :default => "US", :type => :string
114
+ end
115
+ name = ARGV.shift
116
+ Trollop::die "Bucket has not been added because of unknown error" unless create_bucket(service, name, subcommand_options[:location])
117
+ when "remove"
118
+ subcommand_options = Trollop::options do
119
+ banner "remove bucket"
120
+ end
121
+ name = ARGV.shift
122
+ Trollop::die "Bucket name must be given" if name.nil? or name.empty?
123
+ Trollop::die "Bucket has not been removed because of unknown error" unless destroy_bucket(service, name)
124
+ when "show"
125
+ subcommand_options = Trollop::options do
126
+ banner "show bucket"
127
+ opt :prefix, "Limits the response to keys which begin with the indicated prefix", :type => :string
128
+ opt :marker, "Indicates where in the bucket to begin listing", :type => :string
129
+ opt :max_keys, "The maximum number of keys you'd like to see", :type => :integer
130
+ opt :delimiter, "Causes keys that contain the same string between the prefix and the first occurrence of the delimiter to be rolled up into a single result element", :type => :string
131
+ end
132
+ name = ARGV.shift
133
+ Trollop::die "Bucket name must be given" if name.nil? or name.empty?
134
+ show_bucket(service, name, subcommand_options)
135
+ when nil
136
+ list_buckets(service)
137
+ else
138
+ Trollop::die "Unknown subcommand: #{subcommand.inspect}"
139
+ end
140
+ when "object"
141
+ command_options = Trollop::options do
142
+ banner "manage objects"
143
+ stop_on OBJECT_SUBCOMMANDS
144
+ end
145
+ subcommand = ARGV.shift
146
+ case subcommand
147
+ when "add"
148
+ subcommand_options = Trollop::options do
149
+ banner "object add s3_object_name local_file_name"
150
+ opt :type, "A standard MIME type describing the format of the contents", :default => "binary/octet-stream"
151
+ opt :disposition, "Specifies presentational information for the object", :type => :string
152
+ opt :encoding, "Specifies what content encodings have been applied to the object and thus what decoding mechanisms must be applied in order to obtain the media-type referenced by the Content-Type header field", :type => :string
153
+ opt :acl, "The canned ACL to apply to the object. Options include private, public-read, public-read-write, and authenticated-read", :type => :string
154
+ end
155
+ name = ARGV.shift
156
+ Trollop::die "No object name given" if name.nil? or name.empty?
157
+ file_name = ARGV.shift
158
+ Trollop::die "No file name given" if file_name.nil? or file_name.empty?
159
+ Trollop::die "Object has not been added because of unknown error" unless create_object(service, name, file_name, subcommand_options)
160
+ when "remove"
161
+ subcommand_options = Trollop::options do
162
+ banner "object remove s3_object_name"
163
+ end
164
+ name = ARGV.shift
165
+ Trollop::die "No object name given" if name.nil? or name.empty?
166
+ Trollop::die "Object has not been removed because of unknown error" unless destroy_object(service, name)
167
+ when "show"
168
+ subcommand_options = Trollop::options do
169
+ banner "object show s3_object_name optional_file_name"
170
+ end
171
+ name = ARGV.shift
172
+ Trollop::die "No object name given" if name.nil? or name.empty?
173
+ file_name = ARGV.shift
174
+ show_object(service, name, file_name)
175
+ when nil
176
+ list_objects(service)
177
+ else
178
+ Trollop::die "Unknown subcommand: #{subcommand.inspect}"
179
+ end
180
+ when nil
181
+ Trollop::die "No command given"
182
+ else
183
+ Trollop::die "Unknown command #{command.inspect}"
184
+ end
185
+ rescue Error::ResponseError => e
186
+ Trollop::die e.message.sub(/\.+\Z/, "")
187
+ end
@@ -0,0 +1,159 @@
1
+ require "singleton"
2
+ require "stree"
3
+
4
+ # Stree Backend for attachment-fu plugin. After installing attachment-fu
5
+ # plugin, copy the file to:
6
+ # +vendor/plugins/attachment-fu/lib/technoweenie/attachment_fu/backends+
7
+ #
8
+ # To configure StreeBackend create initializer file in your Rails
9
+ # application, e.g. +config/initializers/stree_backend.rb+.
10
+ #
11
+ # Technoweenie::AttachmentFu::Backends::StreeBackend.configuration do |config|
12
+ # config.access_key_id = "..." # your access key id
13
+ # config.secret_access_key = "..." # your secret access key
14
+ # config.bucket_name = "..." # default bucket name to store attachments
15
+ # config.use_ssl = false # pass true if you want to communicate via SSL
16
+ # end
17
+
18
+ module Technoweenie
19
+ module AttachmentFu
20
+ module Backends
21
+ module StreeBackend
22
+
23
+ # StreeBackend configuration class
24
+ class Configuration
25
+ include Singleton
26
+
27
+ ATTRIBUTES = [:access_key_id, :secret_access_key, :use_ssl, :bucket_name]
28
+
29
+ attr_accessor *ATTRIBUTES
30
+ end
31
+
32
+ # Method used to configure StreeBackend, see the example above
33
+ def self.configuration
34
+ if block_given?
35
+ yield Configuration.instance
36
+ end
37
+ Configuration.instance
38
+ end
39
+
40
+ # :nodoc:
41
+ def self.included(base)
42
+ include Stree
43
+
44
+ service = Service.new(:access_key_id => configuration.access_key_id,
45
+ :secret_access_key => configuration.secret_access_key,
46
+ :use_ssl => configuration.use_ssl)
47
+
48
+ bucket_name = base.attachment_options[:bucket_name] || configuration.bucket_name
49
+
50
+ base.cattr_accessor :bucket
51
+ base.bucket = service.buckets.build(bucket_name) # don't connect
52
+
53
+ base.before_update :rename_file
54
+ end
55
+
56
+ # The attachment ID used in the full path of a file
57
+ def attachment_path_id
58
+ ((respond_to?(:parent_id) && parent_id) || id).to_s
59
+ end
60
+
61
+ # The pseudo hierarchy containing the file relative to the bucket name
62
+ # Example: <tt>:table_name/:id</tt>
63
+ def base_path
64
+ [attachment_options[:path_prefix], attachment_path_id].join("/")
65
+ end
66
+
67
+ # The full path to the file relative to the bucket name
68
+ # Example: <tt>:table_name/:id/:filename</tt>
69
+ def full_filename(thumbnail = nil)
70
+ [base_path, thumbnail_name_for(thumbnail)].join("/")
71
+ end
72
+
73
+ # All public objects are accessible via a GET request to the S3 servers. You can generate a
74
+ # url for an object using the s3_url method.
75
+ #
76
+ # @photo.s3_url
77
+ #
78
+ # The resulting url is in the form: <tt>http(s)://:server/:bucket_name/:table_name/:id/:file</tt> where
79
+ # the <tt>:server</tt> variable defaults to <tt>AWS::S3 URL::DEFAULT_HOST</tt> (s3.amazonaws.com) and can be
80
+ # set using the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
81
+ #
82
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
83
+ def s3_url(thumbnail = nil)
84
+ if attachment_options[:cname]
85
+ ["#{s3_protocol}#{bucket.name}", full_filename(thumbnail)].join("/")
86
+ else
87
+ ["#{s3_protocol}#{s3_hostname}#{bucket.path_prefix}", full_filename(thumbnail)].join("/")
88
+ end
89
+ end
90
+ alias :public_url :s3_url
91
+ alias :public_filename :s3_url
92
+
93
+ # Name of the bucket used to store attachments
94
+ def bucket_name
95
+ self.class.bucket.name
96
+ end
97
+
98
+ # :nodoc:
99
+ def create_temp_file
100
+ write_to_temp_file current_data
101
+ end
102
+
103
+ # :nodoc:
104
+ def current_data
105
+ # Object.value full_filename, bucket_name
106
+ object = self.class.bucket.find(full_filename)
107
+ object.content
108
+ end
109
+
110
+ # Returns http:// or https:// depending on use_ssl setting
111
+ def s3_protocol
112
+ attachment_options[:use_ssl] ? "https://" : "http://"
113
+ end
114
+
115
+ # Returns hostname of the bucket
116
+ # e.g. +bucketname.com.s3.amazonaws.com+. Additionally you can
117
+ # pass :cname => true option in has_attachment method to
118
+ # return CNAME only, e.g. +bucketname.com+
119
+ def s3_hostname
120
+ attachment_options[:cname] ? self.class.bucket.name : self.class.bucket.host
121
+ end
122
+
123
+ protected
124
+
125
+ # Frees the space in S3 bucket, used by after_destroy callback
126
+ def destroy_file
127
+ object = self.class.bucket.objects.find(full_filename)
128
+ object.destroy
129
+ end
130
+
131
+ # Renames file if filename has been changed - copy the file to
132
+ # new key and delete old one
133
+ def rename_file
134
+ return unless filename_changed?
135
+
136
+ old_full_filename = [base_path, filename_was].join("/")
137
+
138
+ object = self.class.bucket.objects.find(old_full_filename)
139
+ new_object = object.copy(:key => full_filename, :acl => attachment_options[:acl])
140
+ object.destroy
141
+ true
142
+ end
143
+
144
+ # Saves the file to storage
145
+ def save_to_storage
146
+ if save_attachment?
147
+ object = self.class.bucket.objects.build(full_filename)
148
+
149
+ object.content_type = content_type
150
+ object.acl = attachment_options[:acl]
151
+ object.content = temp_path ? File.open(temp_path) : temp_data
152
+ object.save
153
+ end
154
+ true
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,192 @@
1
+ module Stree
2
+ class Bucket
3
+ extend Roxy::Moxie
4
+ extend Forwardable
5
+
6
+ attr_reader :name, :service
7
+
8
+ def_instance_delegators :service, :service_request
9
+
10
+ # Retrieves the bucket information from the server. Raises an
11
+ # Stree::Error exception if the bucket doesn't exist or you don't
12
+ # have access to it, etc.
13
+ def retrieve
14
+ bucket_request(:get, :params => { :max_keys => 0 })
15
+ self
16
+ end
17
+
18
+ # Returns location of the bucket, e.g. "EU"
19
+ def location(reload = false)
20
+ if reload or @location.nil?
21
+ response = bucket_request(:get, :params => { :location => nil })
22
+ @location = parse_location(response.body)
23
+ else
24
+ @location
25
+ end
26
+ end
27
+
28
+ # Compares the bucket with other bucket. Returns true if the key
29
+ # of the objects are the same, and both have the same buckets (see
30
+ # bucket equality)
31
+ def ==(other)
32
+ self.name == other.name and self.service == other.service
33
+ end
34
+
35
+ # Similar to retrieve, but catches NoSuchBucket exceptions and
36
+ # returns false instead.
37
+ def exists?
38
+ retrieve
39
+ true
40
+ rescue Error::NoSuchBucket
41
+ false
42
+ end
43
+
44
+ # Destroys given bucket. Raises an BucketNotEmpty exception if the
45
+ # bucket is not empty. You can destroy non-empty bucket passing
46
+ # true (to force destroy)
47
+ def destroy(force = false)
48
+ bucket_request(:delete)
49
+ true
50
+ rescue Error::BucketNotEmpty
51
+ if force
52
+ objects.destroy_all
53
+ retry
54
+ else
55
+ raise
56
+ end
57
+ end
58
+
59
+ # Saves the newly built bucket. Optionally you can pass location
60
+ # of the bucket (:eu or :us)
61
+ def save(location = nil)
62
+ location = location.to_s.upcase if location
63
+ options = { :headers => {} }
64
+ if location and location != "US"
65
+ options[:body] = "<CreateBucketConfiguration><LocationConstraint>#{location}</LocationConstraint></CreateBucketConfiguration>"
66
+ options[:headers][:content_type] = "application/xml"
67
+ end
68
+ bucket_request(:put, options)
69
+ true
70
+ end
71
+
72
+ # Returns true if the name of the bucket can be used like VHOST
73
+ # name. If the bucket contains characters like underscore it can't
74
+ # be used as VHOST (e.g. bucket_name.s3.amazonaws.com)
75
+ def vhost?
76
+ "#@name.#{HOST}" =~ /\A#{URI::REGEXP::PATTERN::HOSTNAME}\Z/
77
+ end
78
+
79
+ # Returns host name of the bucket according (see vhost?)
80
+ def host
81
+ vhost? ? "#@name.#{HOST}" : "#{HOST}"
82
+ end
83
+
84
+ # Returns path prefix for non VHOST bucket. Path prefix is used
85
+ # instead of VHOST name,
86
+ # e.g. "bucket_name/"
87
+ def path_prefix
88
+ vhost? ? "" : "#@name/"
89
+ end
90
+
91
+ # Returns the objects in the bucket and caches the result (see
92
+ # reload).
93
+ def objects(reload = false)
94
+ if reload or @objects.nil?
95
+ @objects = fetch_objects
96
+ else
97
+ @objects
98
+ end
99
+ end
100
+
101
+ proxy :objects do
102
+
103
+ # Builds the object in the bucket with given key
104
+ def build(key)
105
+ Object.new(proxy_owner, key)
106
+ end
107
+
108
+ # Finds first object with given name or raises the exception if
109
+ # not found
110
+ def find_first(name)
111
+ object = build(name)
112
+ object.retrieve
113
+ end
114
+ alias :find :find_first
115
+
116
+ # Finds the objects in the bucket.
117
+ # ==== Options:
118
+ # +prefix+:: Limits the response to keys which begin with the indicated prefix
119
+ # +marker+:: Indicates where in the bucket to begin listing
120
+ # +max_keys+:: The maximum number of keys you'd like to see
121
+ # +delimiter+:: Causes keys that contain the same string between the prefix and the first occurrence of the delimiter to be rolled up into a single result element
122
+ def find_all(options = {})
123
+ proxy_owner.send(:fetch_objects, options)
124
+ end
125
+
126
+ # Reloads the object list (clears the cache)
127
+ def reload
128
+ proxy_owner.objects(true)
129
+ end
130
+
131
+ # Destroys all keys in the bucket
132
+ def destroy_all
133
+ proxy_target.each do |object|
134
+ object.destroy
135
+ end
136
+ end
137
+ end
138
+
139
+ def inspect #:nodoc:
140
+ "#<#{self.class}:#{name}>"
141
+ end
142
+
143
+ def initialize(service, name) #:nodoc:
144
+ self.service = service
145
+ self.name = name
146
+ end
147
+
148
+ private
149
+
150
+ attr_writer :service
151
+
152
+ def name=(name)
153
+ raise ArgumentError.new("Invalid bucket name: #{name}") unless name_valid?(name)
154
+ @name = name
155
+ end
156
+
157
+ def bucket_request(method, options = {})
158
+ path = "#{path_prefix}#{options[:path]}"
159
+ service_request(method, options.merge(:host => host, :path => path))
160
+ end
161
+
162
+ def fetch_objects(options = {})
163
+ response = bucket_request(:get, options)
164
+ parse_objects(response.body)
165
+ end
166
+
167
+ def parse_objects(xml_body)
168
+ xml = XmlSimple.xml_in(xml_body)
169
+ objects_attributes = xml["Contents"]
170
+ if objects_attributes
171
+ objects_attributes.map do |object_attributes|
172
+ Object.new(self,
173
+ object_attributes["Key"].first,
174
+ :etag => object_attributes["ETag"].first,
175
+ :last_modified => object_attributes["LastModified"].first,
176
+ :size => object_attributes["Size"].first)
177
+ end
178
+ else
179
+ []
180
+ end
181
+ end
182
+
183
+ def parse_location(xml_body)
184
+ xml = XmlSimple.xml_in(xml_body)
185
+ xml["content"]
186
+ end
187
+
188
+ def name_valid?(name)
189
+ name =~ /\A[a-z0-9][a-z0-9\._-]{2,254}\Z/ and name !~ /\A#{URI::REGEXP::PATTERN::IPV4ADDR}\Z/
190
+ end
191
+ end
192
+ end