direct-to-s3 server side changes
Change-Id: Ie7b415b84f403c98d82f0e67212ae2e7b051b67d Reviewed-on: https://gerrit.instructure.com/2096 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Brian Whitmer <brian@instructure.com>
This commit is contained in:
parent
9168dfbdec
commit
ba735d41b6
|
@ -16,8 +16,11 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
CONTENT_LENGTH_RANGE = 50*1024*1024
|
||||
S3_EXPIRATION_TIME = 30.minutes
|
||||
|
||||
class FilesController < ApplicationController
|
||||
before_filter :require_context, :except => [:public_feed,:full_index,:assessment_question_show, :image_thumbnail]
|
||||
before_filter :require_context, :except => [:public_feed,:full_index,:assessment_question_show, :image_thumbnail,:create_pending,:s3_success]
|
||||
before_filter :check_file_access_flags, :only => :show_relative
|
||||
|
||||
before_filter { |c| c.active_tab = "files" }
|
||||
|
@ -353,6 +356,163 @@ class FilesController < ApplicationController
|
|||
if authorized_action(@attachment, @current_user, :create)
|
||||
end
|
||||
end
|
||||
|
||||
def create_pending
|
||||
@context = Context.find_by_asset_string(params[:attachment][:context_code])
|
||||
@attachment = Attachment.new
|
||||
@attachment.context = @context
|
||||
permission_object = @attachment
|
||||
permission = :create
|
||||
|
||||
# Using workflow_state we can keep track of the files that have been built
|
||||
# but we don't know that there's an s3 component for yet (it's still being
|
||||
# uploaded)
|
||||
workflow_state = 'unattached'
|
||||
# There are multiple reasons why we could be building a file. The default
|
||||
# is to upload it to a context. In the other cases we need to check the
|
||||
# permission related to the purpose to make sure the file isn't being
|
||||
# uploaded just to disappear later
|
||||
if @context.is_a?(Assignment) && params[:intent] == 'comment'
|
||||
permission_object = @context
|
||||
permission = :attach_submission_comment_files
|
||||
elsif @context.is_a?(Assignment) && params[:intent] == 'submit'
|
||||
permission_object = @context
|
||||
permission = (@context.submission_types || "").match(/online_file_upload/) ? :submit : :nothing
|
||||
elsif @context.respond_to?(:is_a_context) && params[:intent] == 'attach_discussion_file'
|
||||
permission_object = @context.discussion_topics.new
|
||||
permission = :attach
|
||||
elsif @context.respond_to?(:is_a_context) && params[:intent] == 'message'
|
||||
permission_object = @context
|
||||
permission = :send_messages
|
||||
workflow_state = 'unattached_temporary'
|
||||
elsif @context.respond_to?(:is_a_context) && params[:intent] && params[:intent] != 'upload'
|
||||
# In other cases (like unzipping a file, extracting a QTI, etc.
|
||||
# we don't actually want the uploaded file to show up in the context's
|
||||
# file listings. If you set its workflow_state to unattached_temporary
|
||||
# then it will never be activated.
|
||||
workflow_state = 'unattached_temporary'
|
||||
end
|
||||
|
||||
if authorized_action(permission_object, @current_user, permission)
|
||||
if @context.respond_to?(:is_a_context) && params[:intent] == 'upload'
|
||||
get_quota
|
||||
return if quota_exceeded(named_context_url(@context, :context_files_url))
|
||||
end
|
||||
@attachment.filename = params[:attachment][:filename]
|
||||
@attachment.file_state = 'deleted'
|
||||
@attachment.workflow_state = workflow_state
|
||||
if @context.respond_to?(:folders)
|
||||
@folder = @context.folders.active.find_by_id(params[:attachment][:folder_id])
|
||||
@folder ||= Folder.unfiled_folder(@context)
|
||||
@attachment.folder_id = @folder.id
|
||||
end
|
||||
@attachment.content_type = Attachment.mimetype(@attachment.filename)
|
||||
@attachment.save!
|
||||
|
||||
if Attachment.s3_storage?
|
||||
# Build the data that will be needed for the user to upload to s3
|
||||
# without us being the middle-man
|
||||
full_filename = @attachment.full_filename.gsub(/\+/, " ")
|
||||
policy = {
|
||||
'expiration' => S3_EXPIRATION_TIME.from_now.utc.iso8601,
|
||||
'conditions' => [
|
||||
{'bucket' => @attachment.bucket_name},
|
||||
{'key' => full_filename},
|
||||
{'acl' => 'private'},
|
||||
['starts-with', '$Filename', ''],
|
||||
['starts-with', '$folder', ''],
|
||||
['content-length-range', 1, CONTENT_LENGTH_RANGE]
|
||||
]
|
||||
}
|
||||
if params[:s3_no_redirect]
|
||||
policy['conditions'] << {'success_action_status' => '201'}
|
||||
else
|
||||
policy['conditions'] << {'success_action_redirect' => s3_success_url(@attachment.id, :uuid => @attachment.uuid)}
|
||||
end
|
||||
if @attachment.content_type && @attachment.content_type != "unknown/unknown"
|
||||
policy['conditions'] << {'content-type' => @attachment.content_type}
|
||||
end
|
||||
policy_encoded = Base64.encode64(policy.to_json).gsub(/\n/, '')
|
||||
signature = Base64.encode64(
|
||||
OpenSSL::HMAC.digest(
|
||||
OpenSSL::Digest::Digest.new('sha1'), AWS::S3::Base.connection.secret_access_key, policy_encoded
|
||||
)
|
||||
).gsub(/\n/, '')
|
||||
res = {
|
||||
:id => @attachment.id,
|
||||
:upload_url => "http://#{@attachment.bucket_name}.s3.amazonaws.com/",
|
||||
:proxied_upload_url => nil,
|
||||
:file_param => 'file',
|
||||
:remote_url => true,
|
||||
:success_url => s3_success_url(@attachment.id, :uuid => @attachment.uuid),
|
||||
:upload_params => {
|
||||
'AWSAccessKeyId' => AWS::S3::Base.connection.access_key_id,
|
||||
'key' => full_filename,
|
||||
'Policy' => policy_encoded,
|
||||
'Filename' => '',
|
||||
'folder' => '',
|
||||
'acl' => 'private',
|
||||
'Signature' => signature
|
||||
}
|
||||
}
|
||||
if params[:s3_no_redirect]
|
||||
res[:upload_params]['success_action_status'] = '201'
|
||||
else
|
||||
res[:upload_params]['success_action_redirect'] = s3_success_url(@attachment.id, :uuid => @attachment.uuid)
|
||||
end
|
||||
if @attachment.content_type && @attachment.content_type != "unknown/unknown"
|
||||
res[:upload_params]['Content-Type'] = @attachment.content_type
|
||||
end
|
||||
render :json => res.to_json
|
||||
|
||||
elsif Attachment.local_storage?
|
||||
render :json => {
|
||||
:id => @attachment.id,
|
||||
:upload_url => named_context_url(@context, :context_files_url, :format => :text) + "?" + { ActionController::Base.session_options[:key] => request.session_options[:id] }.to_query,
|
||||
:remote_url => false,
|
||||
:file_param => 'attachment[uploaded_data]', #uploadify ignores this and uses 'file'
|
||||
:upload_params => {
|
||||
'attachment[folder_id]' => params[:attachment][:folder_id] || '',
|
||||
'folder' => '',
|
||||
'Filename' => '',
|
||||
'attachment[unattached_attachment_id]' => @attachment.id,
|
||||
'authenticity_token' => form_authenticity_token
|
||||
}
|
||||
}.to_json
|
||||
else
|
||||
raise "Unknown storage system configured"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def s3_success
|
||||
@attachment = Attachment.find_by_id_and_workflow_state_and_uuid(params[:id], 'unattached', params[:uuid])
|
||||
details = AWS::S3::S3Object.about(@attachment.full_filename, @attachment.bucket_name) rescue nil
|
||||
if @attachment && details
|
||||
unless @attachment.workflow_state == 'unattached_temporary'
|
||||
@attachment.workflow_state = nil
|
||||
@attachment.file_state = 'available'
|
||||
end
|
||||
@attachment.md5 = (details['etag'] || "").gsub(/\"/, '')
|
||||
@attachment.content_type = details['content-type']
|
||||
@attachment.size = details['content-length']
|
||||
|
||||
if @attachment.md5 && !@attachment.md5.empty? && ns = @attachment.infer_namespace
|
||||
if existing_attachment = Attachment.find_all_by_md5_and_namespace(@attachment.md5, ns).detect{|a| a.id != @attachment.id && !a.root_attachment_id && a.content_type == @attachment.content_type }
|
||||
AWS::S3::S3Object.delete(@attachment.full_filename, @attachment.bucket_name) rescue nil
|
||||
@attachment.root_attachment = existing_attachment
|
||||
@attachment.write_attribute(:filename, nil)
|
||||
@attachment.clear_cached_urls
|
||||
end
|
||||
end
|
||||
|
||||
@attachment.save!
|
||||
@attachment.submit_to_scribd!
|
||||
render_for_text @attachment.to_json(:allow => :uuid, :methods => [:uuid,:readable_size,:mime_class,:currently_locked,:scribdable?], :permissions => {:user => @current_user, :session => session})
|
||||
else
|
||||
render_for_text ""
|
||||
end
|
||||
end
|
||||
|
||||
# POST /files
|
||||
# POST /files.xml
|
||||
|
@ -360,15 +520,19 @@ class FilesController < ApplicationController
|
|||
@folder = @context.folders.active.find_by_id(params[:attachment].delete(:folder_id))
|
||||
@folder ||= Folder.unfiled_folder(@context)
|
||||
params[:attachment][:uploaded_data] ||= params[:attachment_uploaded_data]
|
||||
params[:attachment][:uploaded_data] ||= params[:file]
|
||||
params[:attachment][:user] = @current_user
|
||||
params[:attachment].delete :context_id
|
||||
params[:attachment].delete :context_type
|
||||
@attachment = @context.attachments.new #(params[:attachment])
|
||||
@attachment = @context.attachments.find_by_id_and_workflow_state(params[:attachment].delete(:unattached_attachment_id), 'unattached')
|
||||
@attachment ||= @context.attachments.new #(params[:attachment])
|
||||
if authorized_action(@attachment, @current_user, :create)
|
||||
get_quota
|
||||
return if quota_exceeded(named_context_url(@context, :context_files_url))
|
||||
respond_to do |format|
|
||||
@attachment.folder_id = @folder.id
|
||||
@attachment.folder_id ||= @folder.id
|
||||
@attachment.workflow_state = nil
|
||||
@attachment.file_state = 'available'
|
||||
success = nil
|
||||
if params[:attachment] && params[:attachment][:source_attachment_id]
|
||||
a = Attachment.find(params[:attachment].delete(:source_attachment_id))
|
||||
|
@ -380,7 +544,12 @@ class FilesController < ApplicationController
|
|||
success = @attachment.save
|
||||
end
|
||||
end
|
||||
success = @attachment.update_attributes(params[:attachment]) if params[:attachment][:uploaded_data]
|
||||
if params[:attachment][:uploaded_data]
|
||||
success = @attachment.update_attributes(params[:attachment])
|
||||
@attachment.errors.add_to_base("Upload failed, server error, please try again.") unless success
|
||||
else
|
||||
@attachment.errors.add_to_base("Upload failed, expected form field missing")
|
||||
end
|
||||
unless (@attachment.cacheable_s3_url rescue nil)
|
||||
success = false
|
||||
if (params[:attachment][:uploaded_data].size == 0 rescue false)
|
||||
|
|
|
@ -111,6 +111,7 @@ class SubmissionsController < ApplicationController
|
|||
attachment_ids.each do |id|
|
||||
params[:submission][:attachments] << @current_user.attachments.active.find_by_id(id) if @current_user
|
||||
params[:submission][:attachments] << @group.attachments.active.find_by_id(id) if @group
|
||||
params[:submission][:attachments].compact!
|
||||
end
|
||||
if params[:attachments] && params[:submission][:submission_type] == 'online_upload'
|
||||
|
||||
|
|
|
@ -628,7 +628,7 @@ class Assignment < ActiveRecord::Base
|
|||
self.cached_context_grants_right?(user, session, :participate_as_student) &&
|
||||
!self.locked_for?(user)
|
||||
}
|
||||
set { can :submit }
|
||||
set { can :submit and can :attach_submission_comment_files }
|
||||
|
||||
given { |user, session| !self.locked_for?(user) &&
|
||||
(self.context.allow_student_assignment_edits rescue false) &&
|
||||
|
@ -637,10 +637,10 @@ class Assignment < ActiveRecord::Base
|
|||
set { can :update_content }
|
||||
|
||||
given { |user, session| self.cached_context_grants_right?(user, session, :manage_grades) }#self.context.admins.find_by_id(user) }
|
||||
set { can :update and can :update_content and can :grade and can :delete and can :create and can :read }
|
||||
set { can :update and can :update_content and can :grade and can :delete and can :create and can :read and can :attach_submission_comment_files }
|
||||
|
||||
given { |user, session| self.cached_context_grants_right?(user, session, :manage_assignments) }#self.context.admins.find_by_id(user) }
|
||||
set { can :update and can :update_content and can :delete and can :create and can :read }
|
||||
set { can :update and can :update_content and can :delete and can :create and can :read and can :attach_submission_comment_files }
|
||||
end
|
||||
|
||||
def self.search(query)
|
||||
|
|
|
@ -324,7 +324,7 @@ class Attachment < ActiveRecord::Base
|
|||
def inline_content?
|
||||
self.content_type.match(/\Atext/) || self.extension == '.html' || self.extension == '.htm' || self.extension == '.swf'
|
||||
end
|
||||
|
||||
|
||||
def self.s3_config
|
||||
# Return existing value, even if nil, as long as it's defined
|
||||
return @s3_config if defined?(@s3_config)
|
||||
|
@ -338,11 +338,13 @@ class Attachment < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.s3_storage?
|
||||
file_store_config['storage'] == 's3' && s3_config
|
||||
file_store_config['storage'] == 's3' && s3_config || (RAILS_ENV == "test" && Setting.get("file_storage_test_override", nil) == "s3")
|
||||
end
|
||||
|
||||
def self.local_storage?
|
||||
file_store_config['storage'] == 'local' || !s3_storage?
|
||||
rv = !s3_storage?
|
||||
raise "Unknown storage type!" if rv && file_store_config['storage'] != 'local'
|
||||
rv
|
||||
end
|
||||
|
||||
def downloadable?
|
||||
|
@ -368,6 +370,9 @@ class Attachment < ActiveRecord::Base
|
|||
self.attachment_fu_filename = val
|
||||
end
|
||||
end
|
||||
|
||||
def bucket_name; "no-bucket"; end
|
||||
|
||||
def after_attachment_saved
|
||||
# No point in submitting to scribd since there's not a reliable
|
||||
# URL to provide for referencing
|
||||
|
@ -475,6 +480,12 @@ class Attachment < ActiveRecord::Base
|
|||
h.number_to_human_size(self.size) rescue "size unknown"
|
||||
end
|
||||
|
||||
def clear_cached_urls
|
||||
self.cached_s3_url = nil
|
||||
self.s3_url_cached_at = nil
|
||||
self.cached_scribd_thumbnail = nil
|
||||
end
|
||||
|
||||
def cacheable_s3_url
|
||||
cached = cached_s3_url && s3_url_cached_at && s3_url_cached_at >= (Time.now - 24.hours.to_i)
|
||||
if !cached
|
||||
|
@ -598,6 +609,9 @@ class Attachment < ActiveRecord::Base
|
|||
given { |user, session| self.cached_context_grants_right?(user, session, :read) && !self.locked_for?(user) } #students.include? user }
|
||||
set { can :download }
|
||||
|
||||
given { |user, session| self.context_type == 'Submission' && self.context.grant_rights?(user, session, :comment) }
|
||||
set { can :create }
|
||||
|
||||
given { |user, session|
|
||||
u = session && User.find_by_id(session['file_access_user_id'])
|
||||
u && self.cached_context_grants_right?(u, session, :read) &&
|
||||
|
@ -691,6 +705,8 @@ class Attachment < ActiveRecord::Base
|
|||
state :to_be_zipped
|
||||
state :zipping
|
||||
state :zipped
|
||||
state :unattached
|
||||
state :unattached_temporary
|
||||
end
|
||||
|
||||
named_scope :to_be_zipped, lambda{
|
||||
|
|
|
@ -24,7 +24,7 @@ class Folder < ActiveRecord::Base
|
|||
belongs_to :cloned_item
|
||||
belongs_to :parent_folder, :class_name => "Folder"
|
||||
has_many :file_attachments, :class_name => "Attachment", :order => 'position'
|
||||
has_many :active_file_attachments, :class_name => 'Attachment', :conditions => ['attachments.file_state in (?, ?, ?)', 'available', 'public', 'hidden'], :order => 'position, display_name'
|
||||
has_many :active_file_attachments, :class_name => 'Attachment', :conditions => ['attachments.file_state != ? AND attachments.file_state != ?', 'deleted', 'hidden'], :order => 'position, display_name'
|
||||
has_many :visible_file_attachments, :class_name => 'Attachment', :conditions => ['attachments.file_state in (?, ?)', 'available', 'public'], :order => 'position, display_name'
|
||||
has_many :sub_folders, :class_name => "Folder", :foreign_key => "parent_folder_id", :dependent => :destroy, :order => 'position'
|
||||
has_many :active_sub_folders, :class_name => "Folder", :conditions => ['folders.workflow_state != ?', 'deleted'], :foreign_key => "parent_folder_id", :dependent => :destroy, :order => 'position'
|
||||
|
|
|
@ -507,7 +507,7 @@ class Submission < ActiveRecord::Base
|
|||
# since they're all being held on the assignment for now.
|
||||
attachments ||= []
|
||||
old_ids = (Array(self.attachment_ids || "").join(",")).split(",").map{|id| id.to_i}
|
||||
write_attribute(:attachment_ids, attachments.select{|a| old_ids.include?(a.id) || (a.recently_created? && a.context == self.assignment) || a.context != self.assignment }.map{|a| a.id}.join(","))
|
||||
write_attribute(:attachment_ids, attachments.select{|a| a && a.id && old_ids.include?(a.id) || (a.recently_created? && a.context == self.assignment) || a.context != self.assignment }.map{|a| a.id}.join(","))
|
||||
end
|
||||
|
||||
def validate_single_submission
|
||||
|
|
|
@ -535,6 +535,8 @@ ActionController::Routing::Routes.draw do |map|
|
|||
|
||||
map.calendar 'calendar', :controller => 'calendars', :action => 'show', :conditions => { :method => :get }
|
||||
map.files 'files', :controller => 'files', :action => 'full_index', :conditions => { :method => :get }
|
||||
map.s3_success 'files/s3_success/:id', :controller => 'files', :action => 's3_success'
|
||||
map.file_create_pending 'files/pending', :controller=> 'files', :action => 'create_pending'
|
||||
map.assignments 'assignments', :controller => 'assignments', :action => 'index', :conditions => { :method => :get }
|
||||
|
||||
# The priority is based upon order of creation: first created -> highest priority.
|
||||
|
|
|
@ -235,4 +235,90 @@ describe FilesController do
|
|||
# assigns[:attachment].should be_frozen
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST 'create_pending'" do
|
||||
it "should require authorization" do
|
||||
course_with_teacher(:active_all => true)
|
||||
post 'create_pending', {:attachment => {:context_code => @course.asset_string}}
|
||||
assert_unauthorized
|
||||
end
|
||||
|
||||
it "should create file placeholder (in local mode)" do
|
||||
class Attachment
|
||||
class <<self
|
||||
alias :old_s3_storage? :s3_storage?
|
||||
alias :old_local_storage? :local_storage?
|
||||
end
|
||||
def self.s3_storage?; false; end
|
||||
def self.local_storage?; true; end
|
||||
end
|
||||
begin
|
||||
Attachment.local_storage?.should eql(true)
|
||||
Attachment.s3_storage?.should eql(false)
|
||||
course_with_teacher_logged_in(:active_all => true)
|
||||
post 'create_pending', {:attachment => {
|
||||
:context_code => @course.asset_string,
|
||||
:filename => "bob.txt"
|
||||
}}
|
||||
response.should be_success
|
||||
assigns[:attachment].should_not be_nil
|
||||
assigns[:attachment].id.should_not be_nil
|
||||
json = JSON.parse(response.body) rescue nil
|
||||
json.should_not be_nil
|
||||
json['id'].should eql(assigns[:attachment].id)
|
||||
json['upload_url'].should_not be_nil
|
||||
json['upload_params'].should_not be_nil
|
||||
json['upload_params'].should_not be_empty
|
||||
json['remote_url'].should eql(false)
|
||||
ensure
|
||||
class Attachment
|
||||
class <<self
|
||||
alias :s3_storage? :old_s3_storage?
|
||||
alias :local_storage? :old_local_storage?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "should create file placeholder (in s3 mode)" do
|
||||
class Attachment
|
||||
class <<self
|
||||
alias :old_s3_storage? :s3_storage?
|
||||
alias :old_local_storage? :local_storage?
|
||||
end
|
||||
def self.s3_storage?; true; end
|
||||
def self.local_storage?; false; end
|
||||
end
|
||||
begin
|
||||
Attachment.s3_storage?.should eql(true)
|
||||
Attachment.local_storage?.should eql(false)
|
||||
course_with_teacher_logged_in(:active_all => true)
|
||||
post 'create_pending', {:attachment => {
|
||||
:context_code => @course.asset_string,
|
||||
:filename => "bob.txt"
|
||||
}}
|
||||
response.should be_success
|
||||
assigns[:attachment].should_not be_nil
|
||||
assigns[:attachment].id.should_not be_nil
|
||||
json = JSON.parse(response.body) rescue nil
|
||||
json.should_not be_nil
|
||||
json['id'].should eql(assigns[:attachment].id)
|
||||
json['upload_url'].should_not be_nil
|
||||
json['upload_params'].should_not be_nil
|
||||
json['upload_params'].should_not be_empty
|
||||
json['remote_url'].should eql(true)
|
||||
ensure
|
||||
class Attachment
|
||||
class <<self
|
||||
alias :s3_storage? :old_s3_storage?
|
||||
alias :local_storage? :old_local_storage?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST 's3_success'" do
|
||||
end
|
||||
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue