s3 0.2.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/.document +5 -0
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +48 -0
- data/Rakefile +58 -0
- data/VERSION +1 -0
- data/bin/stree +187 -0
- data/extra/stree_backend.rb +159 -0
- data/lib/stree/bucket.rb +186 -0
- data/lib/stree/connection.rb +199 -0
- data/lib/stree/exceptions.rb +108 -0
- data/lib/stree/object.rb +210 -0
- data/lib/stree/parser.rb +48 -0
- data/lib/stree/roxy/moxie.rb +58 -0
- data/lib/stree/roxy/proxy.rb +72 -0
- data/lib/stree/service.rb +110 -0
- data/lib/stree/signature.rb +157 -0
- data/lib/stree.rb +24 -0
- data/test/bucket_test.rb +231 -0
- data/test/connection_test.rb +164 -0
- data/test/object_test.rb +164 -0
- data/test/service_test.rb +128 -0
- data/test/signature_test.rb +143 -0
- data/test/test_helper.rb +11 -0
- metadata +94 -0
data/.document
ADDED
data/.gitignore
ADDED
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 = "s3"
|
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.2.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.objects.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
|
data/lib/stree/bucket.rb
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
module Stree
|
2
|
+
class Bucket
|
3
|
+
include Parser
|
4
|
+
extend Roxy::Moxie
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_reader :name, :service
|
8
|
+
|
9
|
+
def_instance_delegators :service, :service_request
|
10
|
+
private_class_method :new
|
11
|
+
|
12
|
+
# Retrieves the bucket information from the server. Raises an
|
13
|
+
# Stree::Error exception if the bucket doesn't exist or you don't
|
14
|
+
# have access to it, etc.
|
15
|
+
def retrieve
|
16
|
+
list_bucket(:max_keys => 0)
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns location of the bucket, e.g. "EU"
|
21
|
+
def location(reload = false)
|
22
|
+
if reload or @location.nil?
|
23
|
+
@location = location_constraint
|
24
|
+
else
|
25
|
+
@location
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Compares the bucket with other bucket. Returns true if the key
|
30
|
+
# of the objects are the same, and both have the same buckets (see
|
31
|
+
# bucket equality)
|
32
|
+
def ==(other)
|
33
|
+
self.name == other.name and self.service == other.service
|
34
|
+
end
|
35
|
+
|
36
|
+
# Similar to retrieve, but catches NoSuchBucket exceptions and
|
37
|
+
# returns false instead.
|
38
|
+
def exists?
|
39
|
+
retrieve
|
40
|
+
true
|
41
|
+
rescue Error::NoSuchBucket
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
# Destroys given bucket. Raises an BucketNotEmpty exception if the
|
46
|
+
# bucket is not empty. You can destroy non-empty bucket passing
|
47
|
+
# true (to force destroy)
|
48
|
+
def destroy(force = false)
|
49
|
+
delete_bucket
|
50
|
+
true
|
51
|
+
rescue Error::BucketNotEmpty
|
52
|
+
if force
|
53
|
+
objects.destroy_all
|
54
|
+
retry
|
55
|
+
else
|
56
|
+
raise
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Saves the newly built bucket. Optionally you can pass location
|
61
|
+
# of the bucket (:eu or :us)
|
62
|
+
def save(location = nil)
|
63
|
+
create_bucket_configuration(location)
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns true if the name of the bucket can be used like VHOST
|
68
|
+
# name. If the bucket contains characters like underscore it can't
|
69
|
+
# be used as VHOST (e.g. bucket_name.s3.amazonaws.com)
|
70
|
+
def vhost?
|
71
|
+
"#@name.#{HOST}" =~ /\A#{URI::REGEXP::PATTERN::HOSTNAME}\Z/
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns host name of the bucket according (see vhost?)
|
75
|
+
def host
|
76
|
+
vhost? ? "#@name.#{HOST}" : "#{HOST}"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns path prefix for non VHOST bucket. Path prefix is used
|
80
|
+
# instead of VHOST name,
|
81
|
+
# e.g. "bucket_name/"
|
82
|
+
def path_prefix
|
83
|
+
vhost? ? "" : "#@name/"
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the objects in the bucket and caches the result (see
|
87
|
+
# reload).
|
88
|
+
def objects(reload = false)
|
89
|
+
if reload or @objects.nil?
|
90
|
+
@objects = list_bucket
|
91
|
+
else
|
92
|
+
@objects
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
proxy :objects do
|
97
|
+
|
98
|
+
# Builds the object in the bucket with given key
|
99
|
+
def build(key)
|
100
|
+
Object.send(:new, proxy_owner, :key => key)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Finds first object with given name or raises the exception if
|
104
|
+
# not found
|
105
|
+
def find_first(name)
|
106
|
+
object = build(name)
|
107
|
+
object.retrieve
|
108
|
+
end
|
109
|
+
alias :find :find_first
|
110
|
+
|
111
|
+
# Finds the objects in the bucket.
|
112
|
+
# ==== Options:
|
113
|
+
# +prefix+:: Limits the response to keys which begin with the indicated prefix
|
114
|
+
# +marker+:: Indicates where in the bucket to begin listing
|
115
|
+
# +max_keys+:: The maximum number of keys you'd like to see
|
116
|
+
# +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
|
117
|
+
def find_all(options = {})
|
118
|
+
proxy_owner.send(:list_bucket, options)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Reloads the object list (clears the cache)
|
122
|
+
def reload
|
123
|
+
proxy_owner.objects(true)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Destroys all keys in the bucket
|
127
|
+
def destroy_all
|
128
|
+
proxy_target.each do |object|
|
129
|
+
object.destroy
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def inspect #:nodoc:
|
135
|
+
"#<#{self.class}:#{name}>"
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
attr_writer :service
|
141
|
+
|
142
|
+
def location_constraint
|
143
|
+
response = bucket_request(:get, :params => { :location => nil })
|
144
|
+
parse_location_constraint(response.body)
|
145
|
+
end
|
146
|
+
|
147
|
+
def list_bucket(options = {})
|
148
|
+
response = bucket_request(:get, :params => options)
|
149
|
+
objects_attributes = parse_list_bucket_result(response.body)
|
150
|
+
objects_attributes.map { |object_attributes| Object.send(:new, self, object_attributes) }
|
151
|
+
end
|
152
|
+
|
153
|
+
def create_bucket_configuration(location = nil)
|
154
|
+
location = location.to_s.upcase if location
|
155
|
+
options = { :headers => {} }
|
156
|
+
if location and location != "US"
|
157
|
+
options[:body] = "<CreateBucketConfiguration><LocationConstraint>#{location}</LocationConstraint></CreateBucketConfiguration>"
|
158
|
+
options[:headers][:content_type] = "application/xml"
|
159
|
+
end
|
160
|
+
bucket_request(:put, options)
|
161
|
+
end
|
162
|
+
|
163
|
+
def delete_bucket
|
164
|
+
bucket_request(:delete)
|
165
|
+
end
|
166
|
+
|
167
|
+
def initialize(service, name) #:nodoc:
|
168
|
+
self.service = service
|
169
|
+
self.name = name
|
170
|
+
end
|
171
|
+
|
172
|
+
def name=(name)
|
173
|
+
raise ArgumentError.new("Invalid bucket name: #{name}") unless name_valid?(name)
|
174
|
+
@name = name
|
175
|
+
end
|
176
|
+
|
177
|
+
def bucket_request(method, options = {})
|
178
|
+
path = "#{path_prefix}#{options[:path]}"
|
179
|
+
service_request(method, options.merge(:host => host, :path => path))
|
180
|
+
end
|
181
|
+
|
182
|
+
def name_valid?(name)
|
183
|
+
name =~ /\A[a-z0-9][a-z0-9\._-]{2,254}\Z/ and name !~ /\A#{URI::REGEXP::PATTERN::IPV4ADDR}\Z/
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|