class Raca::Container

Represents a single cloud files container. Contains methods for uploading, downloading, collecting stats, listing files, etc.

You probably don’t want to instantiate this directly, see Raca::Account#containers

Constants

LARGE_FILE_SEGMENT_SIZE
LARGE_FILE_THRESHOLD
MAX_ITEMS_PER_LIST

Attributes

container_name[R]

Public Class Methods

new(account, region, container_name, opts = {}) click to toggle source
   # File lib/raca/container.rb
21 def initialize(account, region, container_name, opts = {})
22   raise ArgumentError, "The container name must not contain '/'." if container_name['/']
23   @account, @region, @container_name = account, region, container_name
24   @storage_url = @account.public_endpoint("cloudFiles", region)
25   @cdn_url     = @account.public_endpoint("cloudFilesCDN", region)
26   @logger = opts[:logger]
27   @logger ||= Rails.logger if defined?(Rails)
28 end

Public Instance Methods

cdn_enable(ttl = 259200) click to toggle source

use this with caution, it will make EVERY object in the container publicly available via the CDN. CDN enabling can be done via the web UI but only with a TTL of 72 hours. Using the API it’s possible to set a TTL of 50 years.

TTL is defined in seconds, default is 72 hours.

    # File lib/raca/container.rb
204 def cdn_enable(ttl = 259200)
205   log "enabling CDN access to #{container_path} with a cache expiry of #{ttl / 60} minutes"
206 
207   response = cdn_client.put(container_path, "X-TTL" => ttl.to_i.to_s)
208   (200..299).cover?(response.code.to_i)
209 end
cdn_metadata() click to toggle source

Return the key details for CDN access to this container. Can be called on non CDN enabled containers, but the details won’t make much sense.

    # File lib/raca/container.rb
185 def cdn_metadata
186   log "retrieving container CDN metadata from #{container_path}"
187   response = cdn_client.head(container_path)
188   {
189     :cdn_enabled => response["X-CDN-Enabled"] == "True",
190     :host => response["X-CDN-URI"],
191     :ssl_host => response["X-CDN-SSL-URI"],
192     :streaming_host => response["X-CDN-STREAMING-URI"],
193     :ttl => response["X-TTL"].to_i,
194     :log_retention => response["X-Log-Retention"] == "True"
195   }
196 end
delete(key) click to toggle source

Delete key from the container. If the container is on the CDN, the object will still be served from the CDN until the TTL expires.

   # File lib/raca/container.rb
50 def delete(key)
51   log "deleting #{key} from #{container_path}"
52   object_path = File.join(container_path, Raca::Util.url_encode(key))
53   response = storage_client.delete(object_path)
54   (200..299).cover?(response.code.to_i)
55 end
download(key, filepath) click to toggle source

Download the object at key into a local file at filepath.

Returns the number of downloaded bytes.

    # File lib/raca/container.rb
 90 def download(key, filepath)
 91   log "downloading #{key} from #{container_path}"
 92   object_path = File.join(container_path, Raca::Util.url_encode(key))
 93   outer_response = storage_client.get(object_path) do |response|
 94     File.open(filepath, 'wb') do |io|
 95       response.read_body do |chunk|
 96         io.write(chunk)
 97       end
 98     end
 99   end
100   outer_response["Content-Length"].to_i
101 end
expiring_url(object_key, temp_url_key, expires_at = Time.now.to_i + 60) click to toggle source

DEPRECATED: use temp_url instead, this will be removed in version 1.0

    # File lib/raca/container.rb
220 def expiring_url(object_key, temp_url_key, expires_at = Time.now.to_i + 60)
221   temp_url(object_key, temp_url_key, expires_at)
222 end
inspect() click to toggle source
    # File lib/raca/container.rb
232 def inspect
233   "#<Raca::Container:#{__id__} region=#{@region} container_name=#{@container_name}>"
234 end
list(options = {}) click to toggle source

Return an array of files in the container.

Supported options

max - the maximum number of items to return marker - return items alphabetically after this key. Useful for pagination prefix - only return items that start with this string details - return extra details for each file - size, md5, etc

    # File lib/raca/container.rb
112 def list(options = {})
113   max = options.fetch(:max, 100_000_000)
114   marker = options.fetch(:marker, nil)
115   prefix = options.fetch(:prefix, nil)
116   details = options.fetch(:details, nil)
117   limit = [max, MAX_ITEMS_PER_LIST].min
118   log "retrieving up to #{max} items from #{container_path}"
119   request_path = list_request_path(marker, prefix, details, limit)
120   result = storage_client.get(request_path).body || ""
121   if details
122     result = JSON.parse(result)
123   else
124     result = result.split("\n")
125   end
126   result.tap {|items|
127     if max <= limit
128       log "Got #{items.length} items; we don't need any more."
129     elsif items.length < limit
130       log "Got #{items.length} items; there can't be any more."
131     else
132       log "Got #{items.length} items; requesting #{limit} more."
133       details ? marker = items.last["name"] : marker = items.last
134       items.concat list(max: max-items.length, marker: marker, prefix: prefix, details: details)
135     end
136   }
137 end
metadata() click to toggle source

Return some basic stats on the current container.

    # File lib/raca/container.rb
151 def metadata
152   log "retrieving container metadata from #{container_path}"
153   response = storage_client.head(container_path)
154   custom = {}
155   response.each_capitalized_name { |name|
156     custom[name] = response[name] if name[/\AX-Container-Meta-/]
157   }
158   {
159     :objects => response["X-Container-Object-Count"].to_i,
160     :bytes => response["X-Container-Bytes-Used"].to_i,
161     :custom => custom,
162   }
163 end
object_metadata(key) click to toggle source

Returns some metadata about a single object in this container.

   # File lib/raca/container.rb
75 def object_metadata(key)
76   object_path = File.join(container_path, Raca::Util.url_encode(key))
77   log "Requesting metadata from #{object_path}"
78 
79   response = storage_client.head(object_path)
80   {
81     :content_type => response["Content-Type"],
82     :bytes => response["Content-Length"].to_i
83   }
84 end
purge_from_akamai(key, email_address) click to toggle source

Remove key from the CDN edge nodes on which it is currently cached. The object is not deleted from the container: as the URL is re-requested, the edge cache will be re-filled with the object currently in the container.

This shouldn’t be used except when it’s really required (e.g. when a piece has to be taken down) because it’s expensive: it lodges a support ticket at Akamai. (!)

   # File lib/raca/container.rb
64 def purge_from_akamai(key, email_address)
65   log "Requesting #{File.join(container_path, key)} to be purged from the CDN"
66   response = cdn_client.delete(
67     File.join(container_path, Raca::Util.url_encode(key)),
68     'X-Purge-Email' => email_address
69   )
70   (200..299).cover?(response.code.to_i)
71 end
set_metadata(headers) click to toggle source

Set metadata headers on the container

headers = { "X-Container-Meta-Access-Control-Allow-Origin" => "*" }
container.set_metadata(headers)

Note: Rackspace requires some headers to begin with ‘X-Container-Meta-’ or other prefixes, e.g. when setting

'Access-Control-Allow-Origin', it needs to be set as 'X-Container-Meta-Access-Control-Allow-Origin'.

See: docs.rackspace.com/files/api/v1/cf-devguide/content/CORS_Container_Header-d1e1300.html

http://docs.rackspace.com/files/api/v1/cf-devguide/content/
  POST_updateacontainermeta_v1__account___container__containerServicesOperations_d1e000.html
    # File lib/raca/container.rb
176 def set_metadata(headers)
177   log "setting headers for container #{container_path}"
178   response = storage_client.post(container_path, '', headers)
179   (200..299).cover?(response.code.to_i)
180 end
temp_upload_url(object_key, temp_url_key, expires_at = Time.now.to_i + 60) click to toggle source

Generate a temporary URL for uploading a file to a private container. Anyone can perform a PUT request to the URL returned from this method and an object will be created in the container.

    # File lib/raca/container.rb
228 def temp_upload_url(object_key, temp_url_key, expires_at = Time.now.to_i + 60)
229   private_url("PUT", object_key, temp_url_key, expires_at)
230 end
temp_url(object_key, temp_url_key, expires_at = Time.now.to_i + 60) click to toggle source

Generate an expiring URL for downloading a file that is otherwise private. Useful for providing temporary access to files.

    # File lib/raca/container.rb
214 def temp_url(object_key, temp_url_key, expires_at = Time.now.to_i + 60)
215   private_url("GET", object_key, temp_url_key, expires_at)
216 end
upload(key, data_or_path, headers = {}) click to toggle source

Upload data_or_path (which may be a filename or an IO) to the container, as key.

If headers are provided they will be added to to upload request. Use this to manually specify content type, content disposition, CORS headers, etc.

   # File lib/raca/container.rb
35 def upload(key, data_or_path, headers = {})
36   if data_or_path.respond_to?(:read) && data_or_path.respond_to?(:size)
37     upload_io(key, data_or_path, data_or_path.size, headers)
38   elsif !File.file?(data_or_path.to_s)
39     raise ArgumentError, "data_or_path must be an IO with data or filename string"
40   else
41     File.open(data_or_path.to_s, "rb") do |io|
42       upload_io(key, io, io.stat.size, headers)
43     end
44   end
45 end

Private Instance Methods

cdn_client() click to toggle source
    # File lib/raca/container.rb
309 def cdn_client
310   @cdn_client ||= @account.http_client(cdn_host)
311 end
cdn_host() click to toggle source
    # File lib/raca/container.rb
331 def cdn_host
332   URI.parse(@cdn_url).host
333 end
cdn_path() click to toggle source
    # File lib/raca/container.rb
335 def cdn_path
336   URI.parse(@cdn_url).path
337 end
container_path() click to toggle source
    # File lib/raca/container.rb
339 def container_path
340   @container_path ||= File.join(storage_path, Raca::Util.url_encode(container_name))
341 end
content_type_needs_cors(path) click to toggle source

Fonts need to be served with CORS headers to work in IE and FF

    # File lib/raca/container.rb
359 def content_type_needs_cors(path)
360   [".eot",".ttf",".woff"].include?(File.extname(path))
361 end
extension_content_type(path) click to toggle source
    # File lib/raca/container.rb
343 def extension_content_type(path)
344   {
345     ".css" => "text/css",
346     ".eot" => "application/vnd.ms-fontobject",
347     ".html" => "text/html",
348     ".js" => "application/javascript",
349     ".png" => "image/png",
350     ".jpg" => "image/jpeg",
351     ".txt" => "text/plain",
352     ".woff" => "font/woff",
353     ".zip" => "application/zip"
354   }[File.extname(path)]
355 end
list_request_path(marker, prefix, details, limit) click to toggle source

build the request path for listing the contents of a container

    # File lib/raca/container.rb
255 def list_request_path(marker, prefix, details, limit)
256   query_string = "limit=#{limit}"
257   query_string += "&marker=#{Raca::Util.url_encode(marker)}" if marker
258   query_string += "&prefix=#{Raca::Util.url_encode(prefix)}" if prefix
259   query_string += "&format=json"      if details
260   container_path + "?#{query_string}"
261 end
log(msg) click to toggle source
    # File lib/raca/container.rb
317 def log(msg)
318   if @logger.respond_to?(:debug)
319     @logger.debug msg
320   end
321 end
md5_io(io) click to toggle source
    # File lib/raca/container.rb
363 def md5_io(io)
364   io.seek(0)
365   digest = Digest::MD5.new
366   # read in 128K chunks
367   io.each(1024 * 128) do |chunk|
368     digest << chunk
369   end
370   io.seek(0)
371   digest.hexdigest
372 end
private_url(method, object_key, temp_url_key, expires_at) click to toggle source
    # File lib/raca/container.rb
238 def private_url(method, object_key, temp_url_key, expires_at)
239   raise ArgumentError, "method must be GET or PUT" unless %w{GET PUT}.include?(method)
240   digest = OpenSSL::Digest.new('sha1')
241 
242   expires = expires_at.to_i
243   path    = File.join(container_path, object_key)
244   encoded_path = File.join(container_path, Raca::Util.url_encode(object_key))
245   data    = "#{method}\n#{expires}\n#{path}"
246 
247   hmac    = OpenSSL::HMAC.new(temp_url_key, digest)
248   hmac << data
249 
250   "https://#{storage_host}#{encoded_path}?temp_url_sig=#{hmac.hexdigest}&temp_url_expires=#{expires}"
251 end
put_upload(full_path, headers, byte_count, io) click to toggle source
    # File lib/raca/container.rb
304 def put_upload(full_path, headers, byte_count, io)
305   response = storage_client.streaming_put(full_path, io, byte_count, headers)
306   response['ETag']
307 end
storage_client() click to toggle source
    # File lib/raca/container.rb
313 def storage_client
314   @storage_client ||= @account.http_client(storage_host)
315 end
storage_host() click to toggle source
    # File lib/raca/container.rb
323 def storage_host
324   URI.parse(@storage_url).host
325 end
storage_path() click to toggle source
    # File lib/raca/container.rb
327 def storage_path
328   URI.parse(@storage_url).path
329 end
upload_io(key, io, byte_count, headers = {}) click to toggle source
    # File lib/raca/container.rb
264 def upload_io(key, io, byte_count, headers = {})
265   if byte_count <= LARGE_FILE_THRESHOLD
266     upload_io_standard(key, io, byte_count, headers)
267   else
268     upload_io_large(key, io, byte_count, headers)
269   end
270 end
upload_io_large(key, io, byte_count, headers = {}) click to toggle source
    # File lib/raca/container.rb
289 def upload_io_large(key, io, byte_count, headers = {})
290   segment_count = (byte_count.to_f / LARGE_FILE_SEGMENT_SIZE).ceil
291   segments = []
292   while segments.size < segment_count
293     start_pos = 0 + (LARGE_FILE_SEGMENT_SIZE * segments.size)
294     segment_key = "%s.%03d" % [key, segments.size]
295     segment_io = WindowedIO.new(io, start_pos, LARGE_FILE_SEGMENT_SIZE)
296     etag = upload_io_standard(segment_key, segment_io, segment_io.size, headers.reject { |k,_v| k == "ETag"})
297     segments << {path: "#{@container_name}/#{segment_key}", etag: etag, size_bytes: segment_io.size}
298   end
299   full_path = File.join(container_path, Raca::Util.url_encode(key)) + "?multipart-manifest=put"
300   manifest_body = StringIO.new(JSON.dump(segments))
301   put_upload(full_path, {}, manifest_body.string.bytesize, manifest_body)
302 end
upload_io_standard(key, io, byte_count, headers = {}) click to toggle source
    # File lib/raca/container.rb
272 def upload_io_standard(key, io, byte_count, headers = {})
273   full_path = File.join(container_path, Raca::Util.url_encode(key))
274 
275   headers['Content-Type']   ||= extension_content_type(full_path)
276   if io.respond_to?(:path)
277     headers['Content-Type'] ||= extension_content_type(io.path)
278   end
279   headers['ETag']           ||= md5_io(io)
280   headers['Content-Type']   ||= "application/octet-stream"
281   if content_type_needs_cors(key)
282     headers['Access-Control-Allow-Origin'] = "*"
283   end
284 
285   log "uploading #{byte_count} bytes to #{full_path}"
286   put_upload(full_path, headers, byte_count, io)
287 end