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
Public Class Methods
# 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
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
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
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 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
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
# File lib/raca/container.rb 232 def inspect 233 "#<Raca::Container:#{__id__} region=#{@region} container_name=#{@container_name}>" 234 end
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
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
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
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
Returns an array of object keys that start with prefix. This is a convenience method that is equivilant to:
container.list(prefix: "foo/bar/")
# File lib/raca/container.rb 144 def search(prefix) 145 log "retrieving container listing from #{container_path} items starting with #{prefix}" 146 list(prefix: prefix) 147 end
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
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
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 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
# File lib/raca/container.rb 309 def cdn_client 310 @cdn_client ||= @account.http_client(cdn_host) 311 end
# File lib/raca/container.rb 331 def cdn_host 332 URI.parse(@cdn_url).host 333 end
# File lib/raca/container.rb 335 def cdn_path 336 URI.parse(@cdn_url).path 337 end
# File lib/raca/container.rb 339 def container_path 340 @container_path ||= File.join(storage_path, Raca::Util.url_encode(container_name)) 341 end
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
# 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
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
# File lib/raca/container.rb 317 def log(msg) 318 if @logger.respond_to?(:debug) 319 @logger.debug msg 320 end 321 end
# 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
# 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
# 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
# File lib/raca/container.rb 313 def storage_client 314 @storage_client ||= @account.http_client(storage_host) 315 end
# File lib/raca/container.rb 323 def storage_host 324 URI.parse(@storage_url).host 325 end
# File lib/raca/container.rb 327 def storage_path 328 URI.parse(@storage_url).path 329 end
# 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
# 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
# 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