Class: S4

Inherits:
Object
  • Object
show all
Defined in:
lib/s4.rb

Overview

Simpler AWS S3 library

Defined Under Namespace

Classes: Error

Constant Summary collapse

VERSION =
"0.0.4"
SubResources =

sub-resource names which may appear in the query string and also must be signed against.

%w( acl location logging notification partNumber policy requestPayment torrent uploadId uploads versionId versioning versions website )
HeaderValues =

Header over-rides which may appear in the query string and also must be signed against (in addition those which begin w/ ‘x-amz-’)

%w( response-content-type response-content-language response-expires reponse-cache-control response-content-disposition response-content-encoding )
BucketACLs =

List of available ACLs on buckets, first is used as default docs.amazonwebservices.com/AmazonS3/latest/API/index.html?RESTBucketPUT.html

%w( private public-read public-read-write authenticated-read bucket-owner-read bucket-owner-full-control )
Policy =

Named policies

{
  public_read: %Q{\{
    "Version": "2008-10-17",
    "Statement": [{
      "Sid": "AllowPublicRead",
      "Effect": "Allow",
      "Principal": {"AWS": "*"},
      "Action": ["s3:GetObject"],
      "Resource": ["arn:aws:s3:::%s/*"]
    }]
  \}}
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(s3_url = ENV["S3_URL"]) ⇒ S4

Returns a new instance of S4.

Raises:

  • (ArgumentError)


83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/s4.rb', line 83

def initialize(s3_url=ENV["S3_URL"])
  raise ArgumentError, "No S3 URL provided. You can set ENV['S3_URL'], too." if s3_url.nil? || s3_url.empty?

  begin
    url = URI(s3_url)
  rescue URI::InvalidURIError => e
    e.message << " The format is s3://access_key_id:[email protected]/bucket"
    raise e
  end

  @access_key_id = url.user
  @secret_access_key = URI.unescape(url.password || "")
  @host = url.host
  @bucket = url.path[1..-1]
end

Instance Attribute Details

#access_key_idObject (readonly)

Returns the value of attribute access_key_id.



37
38
39
# File 'lib/s4.rb', line 37

def access_key_id
  @access_key_id
end

#bucketObject (readonly)

Returns the value of attribute bucket.



37
38
39
# File 'lib/s4.rb', line 37

def bucket
  @bucket
end

#hostObject (readonly)

Returns the value of attribute host.



37
38
39
# File 'lib/s4.rb', line 37

def host
  @host
end

#secret_access_keyObject (readonly)

Returns the value of attribute secret_access_key.



37
38
39
# File 'lib/s4.rb', line 37

def secret_access_key
  @secret_access_key
end

Class Method Details

.connect(options = {}) ⇒ Object

Connect to an S3 bucket.

Pass your S3 connection parameters as URL, or read from ENV if none is passed.

S3_URL format is s3://<access key id>:<secret access key>@s3.amazonaws.com/<bucket>

i.e.

bucket = S4.connect #=> Connects to ENV["S3_URL"]
bucket = S4.connect(url: "s3://0PN5J17HBGZHT7JJ3X82:[email protected]/bucket")


53
54
55
56
57
# File 'lib/s4.rb', line 53

def connect(options={})
  init(options) do |s4|
    s4.connect
  end
end

.create(options = {}) ⇒ Object

Create a new S3 bucket.

See #connect for S3_URL parameters.

Will create the bucket on S3 and connect to it, or just connect if the bucket already exists and is owned by you.

i.e.

bucket = S4.create


68
69
70
71
72
# File 'lib/s4.rb', line 68

def create(options={})
  init(options) do |s4|
    s4.create(options[:acl] || BucketACLs.first)
  end
end

Instance Method Details

#connectObject

Connect to the S3 bucket.

Since S3 doesn’t really require a persistent connection this really just makes sure that it can connect (i.e. the bucket exists and you own it).



103
104
105
# File 'lib/s4.rb', line 103

def connect
  location
end

#create(acl = BucketACLs.first) ⇒ Object

Create the S3 bucket.

If the bucket exists and you own it will not do anything, if it exists and you don’t own it will raise an error.

Optionally pass an ACL for the new bucket, see BucketACLs for valid ACLs.

Default ACL is “private”

Raises:

  • (ArgumentError)


115
116
117
118
119
120
121
122
123
124
# File 'lib/s4.rb', line 115

def create(acl=BucketACLs.first)
  raise ArgumentError.new("Invalid ACL '#{acl}' for bucket. Available ACLs are: #{BucketACLs.join(", ")}.") unless BucketACLs.include?(acl)

  uri = uri("/")
  req = Net::HTTP::Put.new(uri.request_uri)

  req.add_field "x-amz-acl", acl

  request uri, req
end

#delete(name) ⇒ Object

Delete the object with the given name.



149
150
151
# File 'lib/s4.rb', line 149

def delete(name)
  request(uri = uri(name), Net::HTTP::Delete.new(uri.request_uri))
end

#download(name, destination = nil) ⇒ Object

Download the file with the given filename to the given destination.

i.e.

bucket.download("images/palm_trees.jpg", "./palm_trees.jpg")


138
139
140
141
142
143
144
145
146
# File 'lib/s4.rb', line 138

def download(name, destination=nil)
  get(name) do |response|
    File.open(destination || File.join(Dir.pwd, File.basename(name)), "wb") do |io|
      response.read_body do |chunk|
        io.write(chunk)
      end
    end
  end
end

#get(name, &block) ⇒ Object

Lower level object get which just yields the successful S3 response to the block. See #download if you want to simply copy a file from S3 to local.



128
129
130
131
132
# File 'lib/s4.rb', line 128

def get(name, &block)
  request(uri(name), &block)
rescue S4::Error => e
  raise e if e.status != "404"
end

#inspectObject



273
274
275
# File 'lib/s4.rb', line 273

def inspect
  "#<S4: bucket='#{bucket}'>"
end

#list(prefix = "") ⇒ Object

List bucket contents.

Optionally pass a prefix to list from (useful for paths).

i.e.

bucket.list("images/") #=> [ "birds.jpg", "bees.jpg" ]


197
198
199
# File 'lib/s4.rb', line 197

def list(prefix = "")
  REXML::Document.new(request(uri("", query: "prefix=#{prefix}"))).elements.collect("//Key", &:text)
end

#locationObject

Gets information about the buckets location.



266
267
268
269
270
271
# File 'lib/s4.rb', line 266

def location
  response = request uri("/", query: "location")
  location = REXML::Document.new(response).elements["LocationConstraint"].text

  location || "us-east-1"
end

#policyObject

Gets the policy on the bucket.



261
262
263
# File 'lib/s4.rb', line 261

def policy
  request uri("/", query: "policy")
end

#policy=(policy) ⇒ Object

Sets the given policy on the bucket.

Policy can be given as a string which will be applied as given, a hash which will be converted to json, or the name of a pre-defined policy as a symbol.

See S4::Policy for pre-defined policies.

i.e.

$s4 = S4.connect
$s4.policy = :public_read #=> apply named policy
$s4.policy = {"Statement" => "..."} #=> apply policy as hash
$s4.policy = "{\"Statement\": \"...\"}" #=> apply policy as string


249
250
251
252
253
254
255
256
257
258
# File 'lib/s4.rb', line 249

def policy=(policy)
  policy = Policy[policy] % bucket if policy.is_a?(Symbol)

  uri = uri("/", query: "policy")
  req = Net::HTTP::Put.new(uri.request_uri)

  req.body = policy.is_a?(String) ? policy : policy.to_json

  request uri, req
end

#put(io, name, content_type = nil) ⇒ Object

Write an IO stream to a file in this bucket.

Will write file with content_type if given, otherwise will attempt to determine content type by shelling out to POSIX ‘file` command (if IO stream responds to #path). If no content_type could be determined, will default to application/x-www-form-urlencoded.

i.e.

bucket.put(StringIO.new("Awesome!"), "awesome.txt", "text/plain")


174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/s4.rb', line 174

def put(io, name, content_type=nil)
  uri = uri(name)
  req = Net::HTTP::Put.new(uri.request_uri)

  content_type = `file -ib #{io.path}`.chomp if !content_type && io.respond_to?(:path)

  req.add_field "Content-Type", content_type
  req.add_field "Content-Length", io.size
  req.body_stream = io

  target_uri = uri("/#{name}")

  request(target_uri, req)

  target_uri.to_s
end

#upload(name, destination = nil) ⇒ Object

Upload the file with the given filename to the given destination in your S3 bucket.

If no destination is given then uploads it with the same filename to the root of your bucket.

i.e.

bucket.upload("./images/1996_animated_explosion.gif", "website_background.gif")


161
162
163
# File 'lib/s4.rb', line 161

def upload(name, destination=nil)
  put File.open(name, "rb"), destination || File.basename(name)
end

#websiteObject

The URL of the bucket for use as a website.



232
233
234
# File 'lib/s4.rb', line 232

def website
  "#{bucket}.s3-website-#{location}.amazonaws.com"
end

#website!Object

Turns this bucket into a S3 static website bucket.

IMPORTANT: by default a policy will be applied to the bucket allowing read access to all files contained in the bucket.

i.e.

site = S4.connect(url: "s3://0PN5J17HBGZHT7JJ3X82:[email protected]/mywebsite")
site.website!
site.put(StringIO.new("<!DOCTYPE html><html><head><title>Robots!</title></head><body><h1>So many robots!!!</h1></body></html>", "r"), "index.html")
Net::HTTP.get(URI.parse("http://mywebsite.s3.amazonaws.com/")) #=> ...<h1>So many robots!!!</h1>...


211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/s4.rb', line 211

def website!
  self.policy = Policy[:public_read] % bucket

  uri = uri("/", query: "website")
  req = Net::HTTP::Put.new(uri.request_uri)

  req.body = <<-XML
  <WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <IndexDocument>
      <Suffix>index.html</Suffix>
    </IndexDocument>
    <ErrorDocument>
      <Key>404.html</Key>
    </ErrorDocument>
  </WebsiteConfiguration>
  XML

  request uri, req
end