generate safe file urls relative to the shard of the files domain

allows it to be a different shard than where the file is. I had
to remove type casting from dynamic finders that don't know how
to deal with non-integral global ids.

also cache s3 urls on the same shard as the attachment

test plan:
 * have multiple shards and S3 storage
 * have a single safe files domain
 * you should be able to upload and download files in all shards
 * verify that it's going against the files domain, not the normal
   domain

Change-Id: I2b498fc1df20d5b43bf20f702580451621eeaf6a
Reviewed-on: https://gerrit.instructure.com/15158
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Brian Palmer <brianp@instructure.com>
This commit is contained in:
Cody Cutrer 2012-11-08 13:10:01 -07:00
parent 92b914c4ef
commit c1a08e7119
9 changed files with 65 additions and 73 deletions

View File

@ -1052,40 +1052,47 @@ class ApplicationController < ActionController::Base
# escape everything but slashes, see http://code.google.com/p/phusion-passenger/issues/detail?id=113
FILE_PATH_ESCAPE_PATTERN = Regexp.new("[^#{URI::PATTERN::UNRESERVED}/]")
def safe_domain_file_url(attachment, host=nil, verifier = nil, download = false) # TODO: generalize this
res = "#{request.protocol}#{host || HostUrl.file_host(@domain_root_account || Account.default, request.host_with_port)}"
ts, sig = @current_user && @current_user.access_verifier
# add parameters so that the other domain can create a session that
# will authorize file access but not full app access. We need this in
# case there are relative URLs in the file that point to other pieces
# of content.
opts = { :user_id => @current_user.try(:id), :ts => ts, :sf_verifier => sig }
opts[:verifier] = verifier if verifier.present?
if download
# download "for realz, dude" (see later comments about :download)
opts[:download_frd] = 1
else
# don't set :download here, because file_download_url won't like it. see
# comment below for why we'd want to set :download
opts[:inline] = 1
def safe_domain_file_url(attachment, host_and_shard=nil, verifier = nil, download = false) # TODO: generalize this
if !host_and_shard
host_and_shard = HostUrl.file_host_with_shard(@domain_root_account || Account.default, request.host_with_port)
end
host, shard = host_and_shard
res = "#{request.protocol}#{host}"
if @context && Attachment.relative_context?(@context.class.base_ar_class) && @context == attachment.context
# so yeah, this is right. :inline=>1 wants :download=>1 to go along with
# it, so we're setting :download=>1 *because* we want to display inline.
opts[:download] = 1 unless download
shard.activate do
ts, sig = @current_user && @current_user.access_verifier
# if the context is one that supports relative paths (which requires extra
# routes and stuff), then we'll build an actual named_context_url with the
# params for show_relative
res += named_context_url(@context, :context_file_url, attachment)
res += '/' + URI.escape(attachment.full_display_path, FILE_PATH_ESCAPE_PATTERN)
res += '?' + opts.to_query
else
# otherwise, just redirect to /files/:id
res += file_download_url(attachment, opts.merge(:only_path => true))
# add parameters so that the other domain can create a session that
# will authorize file access but not full app access. We need this in
# case there are relative URLs in the file that point to other pieces
# of content.
opts = { :user_id => @current_user.try(:id), :ts => ts, :sf_verifier => sig }
opts[:verifier] = verifier if verifier.present?
if download
# download "for realz, dude" (see later comments about :download)
opts[:download_frd] = 1
else
# don't set :download here, because file_download_url won't like it. see
# comment below for why we'd want to set :download
opts[:inline] = 1
end
if @context && Attachment.relative_context?(@context.class.base_ar_class) && @context == attachment.context
# so yeah, this is right. :inline=>1 wants :download=>1 to go along with
# it, so we're setting :download=>1 *because* we want to display inline.
opts[:download] = 1 unless download
# if the context is one that supports relative paths (which requires extra
# routes and stuff), then we'll build an actual named_context_url with the
# params for show_relative
res += named_context_url(@context, :context_file_url, attachment)
res += '/' + URI.escape(attachment.full_display_path, FILE_PATH_ESCAPE_PATTERN)
res += '?' + opts.to_query
else
# otherwise, just redirect to /files/:id
res += file_download_url(attachment, opts.merge(:only_path => true))
end
end
res

View File

@ -342,7 +342,7 @@ class FilesController < ApplicationController
# won't be able to access or update data with AJAX requests.
def safer_domain_available?
if !@files_domain && request.host_with_port != HostUrl.file_host(@domain_root_account, request.host_with_port)
@safer_domain_host = HostUrl.file_host(@domain_root_account, request.host_with_port)
@safer_domain_host = HostUrl.file_host_with_shard(@domain_root_account, request.host_with_port)
end
!!@safer_domain_host
end

View File

@ -963,24 +963,26 @@ class Attachment < ActiveRecord::Base
end
def cacheable_s3_urls
Rails.cache.fetch(['cacheable_s3_urls', self].cache_key, :expires_in => 24.hours) do
ascii_filename = Iconv.conv("ASCII//TRANSLIT//IGNORE", "UTF-8", display_name)
self.shard.activate do
Rails.cache.fetch(['cacheable_s3_urls', self].cache_key, :expires_in => 24.hours) do
ascii_filename = Iconv.conv("ASCII//TRANSLIT//IGNORE", "UTF-8", display_name)
# response-content-disposition will be url encoded in the depths of
# aws-s3, doesn't need to happen here. we'll be nice and ghetto http
# quote the filename string, though.
quoted_ascii = ascii_filename.gsub(/([\x00-\x1f"\x7f])/, '\\\\\\1')
# response-content-disposition will be url encoded in the depths of
# aws-s3, doesn't need to happen here. we'll be nice and ghetto http
# quote the filename string, though.
quoted_ascii = ascii_filename.gsub(/([\x00-\x1f"\x7f])/, '\\\\\\1')
# awesome browsers will use the filename* and get the proper unicode filename,
# everyone else will get the sanitized ascii version of the filename
quoted_unicode = "UTF-8''#{URI.escape(display_name, /[^A-Za-z0-9.]/)}"
filename = %(filename="#{quoted_ascii}"; filename*=#{quoted_unicode})
# awesome browsers will use the filename* and get the proper unicode filename,
# everyone else will get the sanitized ascii version of the filename
quoted_unicode = "UTF-8''#{URI.escape(display_name, /[^A-Za-z0-9.]/)}"
filename = %(filename="#{quoted_ascii}"; filename*=#{quoted_unicode})
# we need to have versions of the url for each content-disposition
{
'inline' => authenticated_s3_url(:expires_in => 6.days, "response-content-disposition" => "inline; " + filename),
'attachment' => authenticated_s3_url(:expires_in => 6.days, "response-content-disposition" => "attachment; " + filename)
}
# we need to have versions of the url for each content-disposition
{
'inline' => authenticated_s3_url(:expires_in => 6.days, "response-content-disposition" => "inline; " + filename),
'attachment' => authenticated_s3_url(:expires_in => 6.days, "response-content-disposition" => "attachment; " + filename)
}
end
end
end
protected :cacheable_s3_urls

View File

@ -27,7 +27,6 @@ config.to_prepare do
# Raise an exception on finder type mismatch or nil arguments. Helps us catch
# these bugs before they hit.
Canvas.dynamic_finder_nil_arguments_error = :raise
Canvas.dynamic_finder_type_cast_error = :raise
end
# initialize cache store

View File

@ -36,7 +36,6 @@ Canvas.protected_attribute_error = :raise
# Raise an exception on finder type mismatch or nil arguments. Helps us catch
# these bugs before they hit.
Canvas.dynamic_finder_nil_arguments_error = :raise
Canvas.dynamic_finder_type_cast_error = :raise
# eval <env>-local.rb if it exists
Dir[File.dirname(__FILE__) + "/" + File.basename(__FILE__, ".rb") + "-*.rb"].each { |localfile| eval(File.new(localfile).read) }

View File

@ -285,16 +285,7 @@ class ActiveRecord::Base
class << self
def construct_attributes_from_arguments_with_type_cast(attribute_names, arguments)
log_dynamic_finder_nil_arguments(attribute_names) if current_scoped_methods.nil? && arguments.flatten.compact.empty?
attributes = construct_attributes_from_arguments_without_type_cast(attribute_names, arguments)
attributes.each_pair do |attribute, value|
next unless column = columns.detect{ |col| col.name == attribute.to_s }
next if [value].flatten.compact.empty?
cast_value = [value].flatten.map{ |v| v.respond_to?(:quoted_id) ? v : column.type_cast(v) }
cast_value = cast_value.first unless value.is_a?(Array)
next if [value].flatten.map(&:to_s) == [cast_value].flatten.map(&:to_s)
log_dynamic_finder_type_cast(value, column)
attributes[attribute] = cast_value
end
construct_attributes_from_arguments_without_type_cast(attribute_names, arguments)
end
alias_method_chain :construct_attributes_from_arguments, :type_cast
@ -303,12 +294,6 @@ class ActiveRecord::Base
raise DynamicFinderTypeError, error if Canvas.dynamic_finder_nil_arguments_error == :raise
logger.debug "WARNING: " + error
end
def log_dynamic_finder_type_cast(value, column)
error = "Cannot cleanly cast #{value.inspect} to #{column.type} (#{self.base_class}\##{column.name})"
raise DynamicFinderTypeError, error if Canvas.dynamic_finder_type_cast_error == :raise
logger.debug "WARNING: " + error
end
end
def self.merge_includes(first, second)

View File

@ -4,13 +4,6 @@ module Canvas
# this to :raise to raise an exception.
mattr_accessor :protected_attribute_error
# defines the behavior around casting arguments passed into dynamic finders.
# Arguments are coerced to the appropriate type (if the column exists), so
# things like find_by_id('123') become find_by_id(123). The default (:log)
# logs a warning if the cast isn't clean (e.g. '123a' -> 123 or '' -> 0).
# Set this to :raise to raise an error on unclean casts.
mattr_accessor :dynamic_finder_type_cast_error
# defines the behavior when nil or empty array arguments passed into dynamic
# finders. The default (:log) logs a warning if the finder is not scoped and
# if *all* arguments are nil/[], e.g.

View File

@ -79,6 +79,10 @@ class HostUrl
res ||= @@file_host = default_host
end
def file_host_with_shard(account, current_host = nil)
[file_host(account, current_host), Shard.default]
end
def cdn_host
# by default only set it for development. useful so that gravatar can
# proxy our fallback urls

View File

@ -89,7 +89,10 @@ describe ApplicationController do
@controller.expects(:file_download_url).
with(@attachment, @common_params.merge(:inline => 1)).
returns('')
@controller.send(:safe_domain_file_url, @attachment)
HostUrl.expects(:file_host_with_shard).with(42, '').returns(['myfiles', Shard.default])
@controller.instance_variable_set(:@domain_root_account, 42)
url = @controller.send(:safe_domain_file_url, @attachment)
url.should match /myfiles/
end
it "should include :download=>1 in inline urls for relative contexts" do