1274 lines
47 KiB
Ruby
1274 lines
47 KiB
Ruby
#
|
|
# Copyright (C) 2011 Instructure, Inc.
|
|
#
|
|
# This file is part of Canvas.
|
|
#
|
|
# Canvas is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
# Software Foundation, version 3 of the License.
|
|
#
|
|
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
# See the uploads controller and views for examples on how to use this model.
|
|
class Attachment < ActiveRecord::Base
|
|
attr_accessible :context, :folder, :filename, :display_name, :user, :locked, :position, :lock_at, :unlock_at, :uploaded_data
|
|
include HasContentTags
|
|
|
|
belongs_to :context, :polymorphic => true
|
|
belongs_to :cloned_item
|
|
belongs_to :folder
|
|
belongs_to :user
|
|
has_one :account_report
|
|
has_one :media_object
|
|
has_many :submissions
|
|
has_many :attachment_associations
|
|
has_one :context_module_tag, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND workflow_state != ?', 'context_module', 'deleted'], :include => {:context_module => :context_module_progressions}
|
|
belongs_to :root_attachment, :class_name => 'Attachment'
|
|
belongs_to :scribd_mime_type
|
|
belongs_to :scribd_account
|
|
has_one :sis_batch
|
|
has_one :thumbnail, :foreign_key => "parent_id", :conditions => {:thumbnail => "thumb"}
|
|
validates_length_of :cached_s3_url, :maximum => maximum_text_length, :allow_nil => true, :allow_blank => true
|
|
|
|
before_save :infer_display_name
|
|
before_save :default_values
|
|
|
|
before_validation :assert_attachment
|
|
before_destroy :delete_scribd_doc
|
|
acts_as_list :scope => :folder
|
|
after_save :touch_context_if_appropriate
|
|
after_create :build_media_object
|
|
|
|
attr_accessor :podcast_associated_asset
|
|
|
|
# this mixin can be added to a has_many :attachments association, and it'll
|
|
# handle finding replaced attachments. In other words, if an attachment fond
|
|
# by id is deleted but an active attachment in the same context has the same
|
|
# path, it'll return that attachment.
|
|
module FindInContextAssociation
|
|
def find(*a, &b)
|
|
return super if a.first.is_a?(Symbol)
|
|
find_with_possibly_replaced(super)
|
|
end
|
|
|
|
def method_missing(method, *a, &b)
|
|
return super unless method.to_s =~ /^find(?:_all)?_by_id$/
|
|
find_with_possibly_replaced(super)
|
|
end
|
|
|
|
def find_with_possibly_replaced(a_or_as)
|
|
if a_or_as.is_a?(Attachment)
|
|
find_attachment_possibly_replaced(a_or_as)
|
|
elsif a_or_as.is_a?(Array)
|
|
a_or_as.map { |a| find_attachment_possibly_replaced(a) }
|
|
end
|
|
end
|
|
|
|
def find_attachment_possibly_replaced(att)
|
|
# if they found a deleted attachment by id, but there's an available
|
|
# attachment in the same context and the same full path, we return that
|
|
# instead, to emulate replacing a file without having to update every
|
|
# by-id reference in every user content field.
|
|
if att.deleted?
|
|
new_att = Folder.find_attachment_in_context_with_path(proxy_owner, att.full_display_path)
|
|
new_att || att
|
|
else
|
|
att
|
|
end
|
|
end
|
|
end
|
|
|
|
RELATIVE_CONTEXT_TYPES = %w(Course Group User Account)
|
|
# returns true if the context is a type that supports relative file paths
|
|
def self.relative_context?(context_class)
|
|
RELATIVE_CONTEXT_TYPES.include?(context_class.to_s)
|
|
end
|
|
|
|
|
|
def touch_context_if_appropriate
|
|
touch_context unless context_type == 'ConversationMessage'
|
|
end
|
|
|
|
# this is a magic method that gets run by attachment-fu after it is done sending to s3,
|
|
# that is the moment that we also want to submit it to scribd.
|
|
# note, that the time it takes to send to s3 is the bad guy.
|
|
# It blocks and makes the user wait. The good thing is that sending
|
|
# it to scribd from that point does not make the user wait since that
|
|
# does happen asynchronously and the data goes directly from s3 to scribd.
|
|
def after_attachment_saved
|
|
send_later_enqueue_args(:submit_to_scribd!, { :strand => 'scribd', :max_attempts => 1 }) unless Attachment.skip_scribd_submits? || !ScribdAPI.enabled?
|
|
if respond_to?(:process_attachment_with_processing) && thumbnailable? && !attachment_options[:thumbnails].blank? && parent_id.nil?
|
|
temp_file = temp_path || create_temp_file
|
|
self.class.attachment_options[:thumbnails].each { |suffix, size| send_later_if_production(:create_thumbnail_size, suffix) }
|
|
end
|
|
end
|
|
|
|
# this is here becase attachment_fu looks to make sure that parent_id is nil before it will create a thumbnail of something.
|
|
# basically, it makes a false assumption that the thumbnail class is the same as the original class
|
|
# which in our case is false because we use the Thumbnail model for the thumbnails.
|
|
def parent_id;end
|
|
|
|
attr_accessor :clone_updated
|
|
def clone_for(context, dup=nil, options={})
|
|
if !self.cloned_item && !self.new_record?
|
|
self.cloned_item ||= ClonedItem.create(:original_item => self)
|
|
self.save!
|
|
end
|
|
existing = context.attachments.active.find_by_id(self.id)
|
|
existing ||= context.attachments.active.find_by_cloned_item_id(self.cloned_item_id || 0)
|
|
return existing if existing && !options[:overwrite] && !options[:force_copy]
|
|
dup ||= Attachment.new
|
|
dup = existing if existing && options[:overwrite]
|
|
self.attributes.delete_if{|k,v| [:id, :uuid, :folder_id, :user_id, :filename].include?(k.to_sym) }.each do |key, val|
|
|
dup.send("#{key}=", val)
|
|
end
|
|
dup.write_attribute(:filename, self.filename)
|
|
dup.root_attachment_id = self.root_attachment_id || self.id
|
|
dup.context = context
|
|
context.log_merge_result("File \"#{dup.folder.full_name rescue ''}/#{dup.display_name}\" created") if context.respond_to?(:log_merge_result)
|
|
dup.updated_at = Time.now
|
|
dup.clone_updated = true
|
|
dup
|
|
end
|
|
|
|
def build_media_object
|
|
return true if self.class.skip_media_object_creation?
|
|
if self.content_type && self.content_type.match(/\A(video|audio)/)
|
|
MediaObject.send_later(:add_media_files, self, false)
|
|
end
|
|
end
|
|
|
|
def self.process_migration(data, migration)
|
|
attachments = data['file_map'] ? data['file_map']: {}
|
|
# TODO i18n
|
|
to_import = migration.to_import 'files'
|
|
attachments.values.each do |att|
|
|
if !att['is_folder'] && att['migration_id'] && (!to_import || to_import[att['migration_id']])
|
|
begin
|
|
import_from_migration(att, migration.context)
|
|
rescue
|
|
migration.add_warning("Couldn't import file \"#{att[:display_name] || att[:path_name]}\"", $!)
|
|
end
|
|
end
|
|
end
|
|
|
|
if data[:locked_folders]
|
|
data[:locked_folders].each do |path|
|
|
# TODO i18n
|
|
if f = migration.context.active_folders.find_by_full_name("course files/#{path}")
|
|
f.locked = true
|
|
f.save
|
|
end
|
|
end
|
|
end
|
|
if data[:hidden_folders]
|
|
data[:hidden_folders].each do |path|
|
|
# TODO i18n
|
|
if f = migration.context.active_folders.find_by_full_name("course files/#{path}")
|
|
f.workflow_state = 'hidden'
|
|
f.save
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.import_from_migration(hash, context, item=nil)
|
|
hash = hash.with_indifferent_access
|
|
hash[:migration_id] ||= hash[:attachment_id] || hash[:file_id]
|
|
return nil if hash[:migration_id] && hash[:files_to_import] && !hash[:files_to_import][hash[:migration_id]]
|
|
item ||= find_by_context_type_and_context_id_and_id(context.class.to_s, context.id, hash[:id])
|
|
item ||= find_by_context_type_and_context_id_and_migration_id(context.class.to_s, context.id, hash[:migration_id]) if hash[:migration_id]
|
|
item ||= Attachment.find_from_path(hash[:path_name], context)
|
|
if item
|
|
item.context = context
|
|
item.migration_id = hash[:migration_id]
|
|
item.locked = true if hash[:locked]
|
|
item.file_state = 'hidden' if hash[:hidden]
|
|
item.display_name = hash[:display_name] if hash[:display_name]
|
|
item.save_without_broadcasting!
|
|
context.imported_migration_items << item if context.imported_migration_items
|
|
end
|
|
item
|
|
end
|
|
|
|
def assert_attachment
|
|
if !self.to_be_zipped? && !self.zipping? && !self.errored? && (!filename || !content_type || !downloadable?)
|
|
self.errors.add_to_base(t('errors.not_found', "File data could not be found"))
|
|
return false
|
|
end
|
|
end
|
|
|
|
after_create :flag_as_recently_created
|
|
attr_accessor :recently_created
|
|
|
|
validates_presence_of :context_id
|
|
validates_presence_of :context_type
|
|
|
|
serialize :scribd_doc, Scribd::Document
|
|
|
|
def delete_scribd_doc
|
|
return true unless self.scribd_doc && ScribdAPI.enabled? && !self.root_attachment_id
|
|
ScribdAPI.instance.set_user(self.scribd_account)
|
|
self.scribd_doc.destroy
|
|
end
|
|
protected :delete_scribd_doc
|
|
|
|
# This method retrieves a URL to the thumbnail of a document, in a given size, and for any page in that document. Note that docs.getSettings and docs.getList also retrieve thumbnail URLs in default size - this method is really for resizing those. IMPORTANT - it is possible that at some time in the future, Scribd will redesign its image system, invalidating these URLs. So if you cache them, please have an update strategy in place so that you can update them if neceessary.
|
|
#
|
|
# Parameters
|
|
# integer width (optional) Width in px of the desired image. If not included, will use the default thumb size.
|
|
# integer height (optional) Height in px of the desired image. If not included, will use the default thumb size.
|
|
# integer page (optional) Page to generate a thumbnail of. Defaults to 1.
|
|
#
|
|
# usage: Attachment.scribdable?.last.scribd_thumbnail(:height => 1100, :width=> 850, :page => 2)
|
|
# => "http://imgv2-4.scribdassets.com/img/word_document_page/34518627/850x1100/b0c489ddf1/1279739442/2"
|
|
# or just some_attachment.scribd_thumbnail #will give you the default tumbnail for the document.
|
|
def scribd_thumbnail(options={})
|
|
return unless self.scribd_doc && ScribdAPI.enabled?
|
|
if options.empty? && self.cached_scribd_thumbnail
|
|
self.cached_scribd_thumbnail
|
|
else
|
|
begin
|
|
# if we aren't requesting special demensions, fetch and save it to the db.
|
|
if options.empty?
|
|
ScribdAPI.instance.set_user(self.scribd_account)
|
|
self.cached_scribd_thumbnail = self.scribd_doc.thumbnail(options)
|
|
# just update the cached_scribd_thumbnail column of this attachment without running callbacks
|
|
Attachment.update_all({:cached_scribd_thumbnail => self.cached_scribd_thumbnail}, {:id => self.id})
|
|
self.cached_scribd_thumbnail
|
|
else
|
|
Rails.cache.fetch(['scribd_thumb', self, options].cache_key) do
|
|
ScribdAPI.instance.set_user(self.scribd_account)
|
|
self.scribd_doc.thumbnail(options)
|
|
end
|
|
end
|
|
rescue Scribd::NotReadyError
|
|
nil
|
|
rescue => e
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
memoize :scribd_thumbnail
|
|
|
|
def turnitinable?
|
|
self.content_type && [
|
|
'application/msword',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'application/pdf',
|
|
'text/plain',
|
|
'text/html',
|
|
'application/rtf',
|
|
'text/richtext',
|
|
'application/vnd.wordperfect'
|
|
].include?(self.content_type)
|
|
end
|
|
|
|
def flag_as_recently_created
|
|
@recently_created = true
|
|
end
|
|
protected :flag_as_recently_created
|
|
def recently_created?
|
|
@recently_created || (self.created_at && self.created_at > Time.now - (60*5))
|
|
end
|
|
|
|
def scribdable_context?
|
|
case self.context
|
|
when Group
|
|
true
|
|
when User
|
|
true
|
|
when Course
|
|
true
|
|
else
|
|
false
|
|
end
|
|
end
|
|
protected :scribdable_context?
|
|
|
|
def after_extension
|
|
res = self.extension[1..-1] rescue nil
|
|
res = nil if res == "" || res == "unknown"
|
|
res
|
|
end
|
|
|
|
def assert_file_extension
|
|
self.content_type = nil if self.content_type && (self.content_type == 'application/x-unknown' || self.content_type.match(/ERROR/))
|
|
self.content_type ||= self.mimetype(self.filename)
|
|
if self.filename && self.filename.split(".").length < 2
|
|
# we actually have better luck assuming zip files without extensions
|
|
# are docx files than assuming they're zip files
|
|
self.content_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' if self.content_type.match(/zip/)
|
|
ext = self.extension
|
|
self.write_attribute(:filename, self.filename + ext) unless ext == '.unknown'
|
|
end
|
|
end
|
|
def extension
|
|
res = (self.filename || "").match(/(\.[^\.]*)\z/).to_s
|
|
res = nil if res == ""
|
|
if !res || res == ""
|
|
res = File.mime_types[self.content_type].to_s rescue nil
|
|
res = "." + res if res
|
|
end
|
|
res = nil if res == "."
|
|
res ||= ".unknown"
|
|
res.to_s
|
|
end
|
|
|
|
def self.clear_cached_mime_ids
|
|
@@mime_ids = {}
|
|
end
|
|
|
|
def default_values
|
|
self.display_name = nil if self.display_name && self.display_name.empty?
|
|
self.display_name ||= unencoded_filename
|
|
self.file_state ||= "available"
|
|
self.last_unlock_at = self.unlock_at if self.unlock_at
|
|
self.last_lock_at = self.lock_at if self.lock_at
|
|
self.assert_file_extension
|
|
self.folder_id = nil if !self.folder || self.folder.context != self.context
|
|
self.folder_id = nil if self.folder && self.folder.deleted? && !self.deleted?
|
|
self.folder_id ||= Folder.unfiled_folder(self.context).id rescue nil
|
|
self.scribd_attempts ||= 0
|
|
self.folder_id ||= Folder.root_folders(context).first.id rescue nil
|
|
if self.root_attachment && self.new_record?
|
|
[:md5, :size, :content_type, :scribd_mime_type_id, :scribd_user, :submitted_to_scribd_at, :workflow_state, :scribd_doc].each do |key|
|
|
self.send("#{key.to_s}=", self.root_attachment.send(key))
|
|
end
|
|
self.write_attribute(:filename, self.root_attachment.filename)
|
|
end
|
|
self.context = self.folder.context if self.folder && (!self.context || (self.context.respond_to?(:is_a_context? ) && self.context.is_a_context?))
|
|
|
|
if !self.scribd_mime_type_id && !['text/html', 'application/xhtml+xml', 'application/xml', 'text/xml'].include?(self.content_type)
|
|
@@mime_ids ||= {}
|
|
@@mime_ids[self.content_type] ||= self.content_type && ScribdMimeType.find_by_name(self.content_type).try(:id)
|
|
self.scribd_mime_type_id = @@mime_ids[self.content_type]
|
|
if !self.scribd_mime_type_id
|
|
@@mime_ids[self.after_extension] ||= self.after_extension && ScribdMimeType.find_by_extension(self.after_extension).try(:id)
|
|
self.scribd_mime_type_id = @@mime_ids[self.after_extension]
|
|
end
|
|
end
|
|
|
|
if self.respond_to?(:namespace=) && self.new_record?
|
|
self.namespace = infer_namespace
|
|
end
|
|
|
|
self.media_entry_id ||= 'maybe' if self.new_record? && self.content_type && self.content_type.match(/(video|audio)/)
|
|
|
|
# Raise an error if this is scribdable without a scribdable context?
|
|
if scribdable_context? and scribdable? and ScribdAPI.enabled?
|
|
unless context.scribd_account
|
|
ScribdAccount.create(:scribdable => context)
|
|
self.context.reload
|
|
end
|
|
self.scribd_account_id ||= context.scribd_account.id
|
|
end
|
|
end
|
|
protected :default_values
|
|
|
|
def infer_namespace
|
|
ns = Attachment.domain_namespace
|
|
ns ||= self.context.root_account.file_namespace rescue nil
|
|
ns ||= self.context.account.file_namespace rescue nil
|
|
if Rails.env.development? && Attachment.local_storage?
|
|
ns ||= ""
|
|
ns = "_localstorage_/#{ns}"
|
|
end
|
|
ns = nil if ns && ns.empty?
|
|
ns
|
|
end
|
|
|
|
def process_s3_details!(details)
|
|
unless workflow_state == 'unattached_temporary'
|
|
self.workflow_state = nil
|
|
self.file_state = 'available'
|
|
end
|
|
self.md5 = (details['etag'] || "").gsub(/\"/, '')
|
|
self.content_type = details['content-type']
|
|
self.size = details['content-length']
|
|
|
|
if md5.present? && ns = infer_namespace
|
|
if existing_attachment = Attachment.find_all_by_md5_and_namespace(md5, ns).detect{|a| a.id != id && !a.root_attachment_id && a.content_type == content_type }
|
|
AWS::S3::S3Object.delete(full_filename, bucket_name) rescue nil
|
|
self.root_attachment = existing_attachment
|
|
write_attribute(:filename, nil)
|
|
clear_cached_urls
|
|
end
|
|
end
|
|
|
|
save!
|
|
|
|
# normally this would be called by attachment_fu after it had uploaded the file to S3.
|
|
after_attachment_saved
|
|
end
|
|
|
|
CONTENT_LENGTH_RANGE = 10.gigabytes
|
|
S3_EXPIRATION_TIME = 30.minutes
|
|
|
|
def ajax_upload_params(pseudonym, local_upload_url, s3_success_url, options = {})
|
|
if Attachment.s3_storage?
|
|
res = {
|
|
:upload_url => "#{options[:ssl] ? "https" : "http"}://#{bucket_name}.s3.amazonaws.com/",
|
|
:remote_url => true,
|
|
:file_param => 'file',
|
|
:success_url => s3_success_url,
|
|
:upload_params => {
|
|
'AWSAccessKeyId' => AWS::S3::Base.connection.access_key_id
|
|
}
|
|
}
|
|
elsif Attachment.local_storage?
|
|
res = {
|
|
:upload_url => local_upload_url,
|
|
:remote_url => false,
|
|
:file_param => options[:file_param] || 'attachment[uploaded_data]', #uploadify ignores this and uses 'file',
|
|
:upload_params => options[:upload_params] || {}
|
|
}
|
|
else
|
|
raise "Unknown storage system configured"
|
|
end
|
|
|
|
# Build the data that will be needed for the user to upload to s3
|
|
# without us being the middle-man
|
|
sanitized_filename = full_filename.gsub(/\+/, " ")
|
|
policy = {
|
|
'expiration' => (options[:expiration] || S3_EXPIRATION_TIME).from_now.utc.iso8601,
|
|
'conditions' => [
|
|
{'bucket' => bucket_name},
|
|
{'key' => sanitized_filename},
|
|
{'acl' => 'private'},
|
|
['starts-with', '$Filename', ''],
|
|
['starts-with', '$folder', ''],
|
|
['content-length-range', 1, (options[:max_size] || CONTENT_LENGTH_RANGE)]
|
|
]
|
|
}
|
|
extras = []
|
|
if options[:no_redirect]
|
|
extras << {'success_action_status' => '201'}
|
|
elsif res[:success_url]
|
|
extras << {'success_action_redirect' => res[:success_url]}
|
|
end
|
|
if content_type && content_type != "unknown/unknown"
|
|
extras << {'content-type' => content_type}
|
|
end
|
|
policy['conditions'] += extras
|
|
# flash won't send the session cookie, so for local uploads we put the user id in the signed
|
|
# policy so we can mock up the session for FilesController#create
|
|
policy['conditions'] << {'pseudonym_id' => pseudonym.id} if Attachment.local_storage?
|
|
|
|
policy_encoded = Base64.encode64(policy.to_json).gsub(/\n/, '')
|
|
signature = Base64.encode64(
|
|
OpenSSL::HMAC.digest(
|
|
OpenSSL::Digest::Digest.new('sha1'), Attachment.shared_secret, policy_encoded
|
|
)
|
|
).gsub(/\n/, '')
|
|
|
|
res[:id] = id
|
|
res[:upload_params].merge!({
|
|
'Filename' => '',
|
|
'folder' => '',
|
|
'key' => sanitized_filename,
|
|
'acl' => 'private',
|
|
'Policy' => policy_encoded,
|
|
'Signature' => signature,
|
|
})
|
|
extras.map(&:to_a).each{ |extra| res[:upload_params][extra.first.first] = extra.first.last }
|
|
res
|
|
end
|
|
|
|
def unencoded_filename
|
|
CGI::unescape(self.filename || t(:default_filename, "File"))
|
|
end
|
|
|
|
def handle_duplicates(method)
|
|
return [] unless method.present? && self.folder
|
|
method = method.to_sym
|
|
deleted_attachments = []
|
|
other_attachments =
|
|
if method == :rename
|
|
self.display_name = Attachment.make_unique_filename(self.display_name, self.folder.active_file_attachments.reject {|a| a.id == self.id }.map(&:display_name))
|
|
self.save
|
|
elsif method == :overwrite
|
|
self.folder.active_file_attachments.find_all_by_display_name(self.display_name).reject {|a| a.id == self.id }.each do |a|
|
|
a.destroy
|
|
deleted_attachments << a
|
|
end
|
|
end
|
|
return deleted_attachments
|
|
end
|
|
|
|
def self.destroy_files(ids)
|
|
Attachment.find_all_by_id(ids).compact.each(&:destroy)
|
|
end
|
|
|
|
before_save :assign_uuid
|
|
def assign_uuid
|
|
self.uuid ||= AutoHandle.generate_securish_uuid
|
|
end
|
|
protected :assign_uuid
|
|
|
|
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)
|
|
@s3_config ||= YAML.load_file(RAILS_ROOT + "/config/amazon_s3.yml")[RAILS_ENV] rescue nil
|
|
end
|
|
|
|
def self.file_store_config
|
|
# Return existing value, even if nil, as long as it's defined
|
|
@file_store_config ||= YAML.load_file(RAILS_ROOT + "/config/file_store.yml")[RAILS_ENV] rescue nil
|
|
@file_store_config ||= { 'storage' => 'local' }
|
|
# default the secure setting to true only in production
|
|
@file_store_config['secure'] = Rails.env.production? unless @file_store_config.has_key?('secure')
|
|
@file_store_config['path_prefix'] ||= @file_store_config['path'] || 'tmp/files'
|
|
if RAILS_ENV == "test"
|
|
# yes, a rescue nil; the problem is that in an automated test environment, this may be
|
|
# in the auto-require path, before the DB is even created; obviously it hasn't been
|
|
# overridden yet
|
|
file_storage_test_override = Setting.get("file_storage_test_override", nil) rescue nil
|
|
return @file_store_config.merge({"storage" => file_storage_test_override}) if file_storage_test_override
|
|
end
|
|
return @file_store_config
|
|
end
|
|
|
|
def self.s3_storage?
|
|
(file_store_config['storage'] rescue nil) == 's3' && s3_config
|
|
end
|
|
|
|
def self.local_storage?
|
|
rv = !s3_storage?
|
|
raise "Unknown storage type!" if rv && file_store_config['storage'] != 'local'
|
|
rv
|
|
end
|
|
|
|
def self.shared_secret
|
|
self.s3_storage? ? AWS::S3::Base.connection.secret_access_key : "local_storage" + Canvas::Security.encryption_key
|
|
end
|
|
|
|
def downloadable?
|
|
!!(self.authenticated_s3_url rescue false)
|
|
end
|
|
|
|
# Haaay... you're changing stuff here? Don't forget about the Thumbnail model
|
|
# too, it cares about local vs s3 storage.
|
|
if local_storage?
|
|
has_attachment(
|
|
:path_prefix => file_store_config['path_prefix'],
|
|
:thumbnails => { :thumb => '200x50' },
|
|
:thumbnail_class => 'Thumbnail'
|
|
)
|
|
def authenticated_s3_url(*args)
|
|
return root_attachment.authenticated_s3_url(*args) if root_attachment
|
|
protocol = args[0].is_a?(Hash) && args[0][:protocol]
|
|
protocol ||= self.class.file_store_config['secure'] ? "https://" : "http://"
|
|
protocol ||= "//"
|
|
"#{protocol}#{HostUrl.context_host(context)}/#{context_type.underscore.pluralize}/#{context_id}/files/#{id}/download?verifier=#{uuid}"
|
|
end
|
|
|
|
alias_method :attachment_fu_filename=, :filename=
|
|
def filename=(val)
|
|
if self.new_record?
|
|
write_attribute(:filename, val)
|
|
else
|
|
self.attachment_fu_filename = val
|
|
end
|
|
end
|
|
|
|
def bucket_name; "no-bucket"; end
|
|
else
|
|
has_attachment(
|
|
:storage => :s3,
|
|
:s3_access => :private,
|
|
:thumbnails => { :thumb => '200x50' },
|
|
:thumbnail_class => 'Thumbnail'
|
|
)
|
|
end
|
|
|
|
# Returns an IO-like object containing the contents of the attachment file.
|
|
# Any resources are guaranteed to be cleaned up when the object is garbage
|
|
# collected (for instance, using the Tempfile class). Calling close on the
|
|
# object may clean up things faster.
|
|
#
|
|
# By default, this method will stream the file as it is read, if it's stored
|
|
# remotely and streaming is possible. If opts[:need_local_file] is true,
|
|
# then a local Tempfile will be created if necessary and the IO object
|
|
# returned will always respond_to :path and :rewind, and have the right file
|
|
# extension.
|
|
#
|
|
# Be warned! If local storage is used, a File handle to the actual file will
|
|
# be returned, not a Tempfile handle. So don't rm the file's .path or
|
|
# anything crazy like that. If you need to test whether you can move the file
|
|
# at .path, or if you need to copy it, check if the file is_a?(Tempfile) (and
|
|
# pass :need_local_file => true of course).
|
|
#
|
|
# If opts[:temp_folder] is given, and a local temporary file is created, this
|
|
# path will be used instead of the default system temporary path. It'll be
|
|
# created if necessary.
|
|
def open(opts = {})
|
|
if Attachment.local_storage?
|
|
File.open(self.full_filename, 'rb')
|
|
else
|
|
# TODO: !need_local_file -- net/http and thus AWS::S3::S3Object don't
|
|
# natively support streaming the response, except when a block is given.
|
|
# so without Fibers, there's not a great way to return an IO-like object
|
|
# that streams the response. A separate thread, I guess. Bleck. Need to
|
|
# investigate other options.
|
|
if opts[:temp_folder].present? && !File.exist?(opts[:temp_folder])
|
|
FileUtils.mkdir_p(opts[:temp_folder])
|
|
end
|
|
tempfile = Tempfile.new(["attachment_#{self.id}", self.extension],
|
|
opts[:temp_folder].presence || Dir::tmpdir)
|
|
AWS::S3::S3Object.stream(self.full_filename, self.bucket_name) do |chunk|
|
|
tempfile.write(chunk)
|
|
end
|
|
tempfile.rewind
|
|
tempfile
|
|
end
|
|
end
|
|
|
|
# you should be able to pass an optional width, height, and page_number/video_seconds to this method
|
|
# can't handle arbitrary thumbnails for our attachment_fu thumbnails on s3 though, we could handle a couple *predefined* sizes though
|
|
def thumbnail_url(options={})
|
|
return nil if Attachment.skip_thumbnails || !ScribdAPI.enabled?
|
|
if self.scribd_doc #handle if it is a scribd doc, get the thumbnail from scribd's api
|
|
self.scribd_thumbnail(options)
|
|
elsif self.thumbnail #handle attachment_fu iamges that we have made a thubnail for on our s3
|
|
self.thumbnail.cached_s3_url
|
|
elsif self.media_object && self.media_object.media_id
|
|
Kaltura::ClientV3.new.thumbnail_url(self.media_object.media_id,
|
|
:width => options[:width] || 140,
|
|
:height => options[:height] || 100,
|
|
:vid_sec => options[:video_seconds] || 5)
|
|
else
|
|
# "still need to handle things that are not images with thumbnails, scribd_docs, or kaltura docs"
|
|
end
|
|
end
|
|
memoize :thumbnail_url
|
|
|
|
alias_method :original_sanitize_filename, :sanitize_filename
|
|
def sanitize_filename(filename)
|
|
filename = CGI::escape(filename)
|
|
filename = self.root_attachment.filename if self.root_attachment && self.root_attachment.filename
|
|
chunks = (filename || "").scan(/\./).length + 1
|
|
filename.gsub!(/[^\.]+/) do |str|
|
|
str[0, 220/chunks]
|
|
end
|
|
filename
|
|
end
|
|
has_a_broadcast_policy
|
|
|
|
set_broadcast_policy do |p|
|
|
p.dispatch :new_file_added
|
|
p.to { context.participants - [user] }
|
|
p.whenever { |record|
|
|
!@skip_broadcast_messages and
|
|
record.context.state == :available and record.just_created and
|
|
record.folder.visible?
|
|
}
|
|
end
|
|
|
|
def infer_display_name
|
|
self.display_name ||= unencoded_filename
|
|
end
|
|
protected :infer_display_name
|
|
|
|
# Accepts an array of words and returns an array of words, some of them
|
|
# combined by a dash.
|
|
def dashed_map(words, n=30)
|
|
line_length = 0
|
|
words.inject([]) do |list, word|
|
|
|
|
# Get the length of the word
|
|
word_size = word.size
|
|
# Add 1 for the space preceding the word
|
|
# There is no space added before the first word
|
|
word_size += 1 unless list.empty?
|
|
|
|
# If adding a word takes us over our limit,
|
|
# join two words by a dash and insert that
|
|
if word_size >= n
|
|
word_pieces = []
|
|
((word_size / 15) + 1).times do |i|
|
|
word_pieces << word[(i * 15)..(((i+1) * 15)-1)]
|
|
end
|
|
word = word_pieces.compact.select{|p| p.length > 0}.join('-')
|
|
list << word
|
|
line_length = word.size
|
|
elsif (line_length + word_size >= n) and not list.empty?
|
|
previous = list.pop
|
|
previous ||= ''
|
|
list << previous + '-' + word
|
|
line_length = word_size
|
|
# Otherwise just add the word to the list
|
|
else
|
|
list << word
|
|
line_length += word_size
|
|
end
|
|
|
|
# Return the list so that inject works
|
|
list
|
|
end
|
|
end
|
|
protected :dashed_map
|
|
|
|
|
|
def readable_size
|
|
h = ActionView::Base.new
|
|
h.extend ActionView::Helpers::NumberHelper
|
|
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
|
|
# 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.
|
|
raw_filename = self.display_name
|
|
quoted_filename = raw_filename.gsub(/([\x00-\x1f"\x7f])/, '\\\\\\1')
|
|
quoted_filename = "\"#{quoted_filename}\"" unless quoted_filename == raw_filename
|
|
self.cached_s3_url = authenticated_s3_url(:expires_in => 144.hours,
|
|
'response-content-disposition' => "attachment; filename=#{quoted_filename}")
|
|
self.s3_url_cached_at = Time.now
|
|
save
|
|
end
|
|
cached_s3_url
|
|
end
|
|
|
|
def attachment_path_id
|
|
a = (self.respond_to?(:root_attachment) && self.root_attachment) || self
|
|
((a.respond_to?(:parent_id) && a.parent_id) || a.id).to_s
|
|
end
|
|
|
|
def filename
|
|
read_attribute(:filename) || (self.root_attachment && self.root_attachment.filename)
|
|
end
|
|
|
|
def thumbnail_with_root_attachment
|
|
self.thumbnail_without_root_attachment || self.root_attachment.try(:thumbnail)
|
|
end
|
|
alias_method_chain :thumbnail, :root_attachment
|
|
|
|
def scribd_doc
|
|
self.read_attribute(:scribd_doc) || self.root_attachment.try(:scribd_doc)
|
|
end
|
|
|
|
def content_directory
|
|
self.directory_name || Folder.root_folders(self.context).first.name
|
|
end
|
|
|
|
def to_atom(opts={})
|
|
Atom::Entry.new do |entry|
|
|
entry.title = t(:feed_title_with_context, "File, %{course_or_group}: %{title}", :course_or_group => self.context.name, :title => self.context.name) if opts[:include_context]
|
|
entry.title = t(:feed_title, "File: %{title}", :title => self.context.name) unless opts[:include_context]
|
|
entry.updated = self.updated_at
|
|
entry.published = self.created_at
|
|
entry.id = "tag:#{HostUrl.default_host},#{self.created_at.strftime("%Y-%m-%d")}:/files/#{self.feed_code}"
|
|
entry.links << Atom::Link.new(:rel => 'alternate',
|
|
:href => "http://#{HostUrl.context_host(self.context)}/#{context_url_prefix}/files/#{self.id}")
|
|
entry.content = Atom::Content::Html.new("#{self.display_name}")
|
|
end
|
|
end
|
|
|
|
def name
|
|
display_name
|
|
end
|
|
|
|
def title
|
|
display_name
|
|
end
|
|
|
|
def associate_with(context)
|
|
self.attachment_associations.create(:context => context)
|
|
end
|
|
|
|
def mime_class
|
|
{
|
|
'text/html' => 'html',
|
|
"text/x-csharp" => "code",
|
|
"text/xml" => "code",
|
|
"text/css" => 'code',
|
|
"text" => "text",
|
|
"text/plain" => "text",
|
|
"application/rtf" => "doc",
|
|
"text/rtf" => "doc",
|
|
"application/vnd.oasis.opendocument.text" => "doc",
|
|
"application/pdf" => "pdf",
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" => "doc",
|
|
"application/x-docx" => "doc",
|
|
"application/msword" => "doc",
|
|
"application/vnd.ms-powerpoint" => "ppt",
|
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation" => "ppt",
|
|
"application/vnd.ms-excel" => "xls",
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" => "xls",
|
|
"application/vnd.oasis.opendocument.spreadsheet" => "xls",
|
|
"image/jpeg" => "image",
|
|
"image/pjpeg" => "image",
|
|
"image/png" => "image",
|
|
"image/gif" => "image",
|
|
"image/x-psd" => "image",
|
|
"application/x-rar" => "zip",
|
|
"application/x-rar-compressed" => "zip",
|
|
"application/x-zip" => "zip",
|
|
"application/x-zip-compressed" => "zip",
|
|
"application/xml" => "code",
|
|
"application/zip" => "zip",
|
|
"audio/mpeg" => "audio",
|
|
"audio/basic" => "audio",
|
|
"audio/mid" => "audio",
|
|
"audio/mpeg" => "audio",
|
|
"audio/3gpp" => "audio",
|
|
"audio/x-aiff" => "audio",
|
|
"audio/x-mpegurl" => "audio",
|
|
"audio/x-pn-realaudio" => "audio",
|
|
"audio/x-wav" => "audio",
|
|
"video/mpeg" => "video",
|
|
"video/quicktime" => "video",
|
|
"video/x-la-asf" => "video",
|
|
"video/x-ms-asf" => "video",
|
|
"video/x-msvideo" => "video",
|
|
"video/x-sgi-movie" => "video",
|
|
"video/3gpp" => "video",
|
|
"video/mp4" => "video",
|
|
"application/x-shockwave-flash" => "flash"
|
|
}[content_type] || "file"
|
|
end
|
|
|
|
set_policy do
|
|
given { |user, session| self.cached_context_grants_right?(user, session, :manage_files) } #admins.include? user }
|
|
can :read and can :update and can :delete and can :create and can :download
|
|
|
|
given { |user, session| self.public? }
|
|
can :read and can :download
|
|
|
|
given { |user, session| self.cached_context_grants_right?(user, session, :read) } #students.include? user }
|
|
can :read
|
|
|
|
given { |user, session|
|
|
self.cached_context_grants_right?(user, session, :read) &&
|
|
(self.cached_context_grants_right?(user, session, :manage_files) || !self.locked_for?(user))
|
|
}
|
|
can :download
|
|
|
|
given { |user, session| self.context_type == 'Submission' && self.context.grant_rights?(user, session, :comment) }
|
|
can :create
|
|
|
|
given { |user, session|
|
|
session && session['file_access_user_id'].present? &&
|
|
(u = User.find_by_id(session['file_access_user_id'])) &&
|
|
self.cached_context_grants_right?(u, session, :read) &&
|
|
session['file_access_expiration'] && session['file_access_expiration'].to_i > Time.now.to_i
|
|
}
|
|
can :read
|
|
|
|
given { |user, session|
|
|
session && session['file_access_user_id'].present? &&
|
|
(u = User.find_by_id(session['file_access_user_id'])) &&
|
|
self.cached_context_grants_right?(u, session, :read) &&
|
|
(self.cached_context_grants_right?(u, session, :manage_files) || !self.locked_for?(u)) &&
|
|
session['file_access_expiration'] && session['file_access_expiration'].to_i > Time.now.to_i
|
|
}
|
|
can :download
|
|
end
|
|
|
|
def locked_for?(user, opts={})
|
|
@locks ||= {}
|
|
return false if opts[:check_policies] && self.grants_right?(user, nil, :update)
|
|
return {:manually_locked => true} if self.locked || (self.folder && self.folder.locked?)
|
|
@locks[user ? user.id : 0] ||= Rails.cache.fetch(['_locked_for', self, user].cache_key, :expires_in => 1.minute) do
|
|
locked = false
|
|
if (self.unlock_at && Time.now < self.unlock_at)
|
|
locked = {:asset_string => self.asset_string, :unlock_at => self.unlock_at}
|
|
elsif (self.lock_at && Time.now > self.lock_at)
|
|
locked = {:asset_string => self.asset_string, :lock_at => self.lock_at}
|
|
elsif (self.could_be_locked && self.context_module_tag && !self.context_module_tag.available_for?(user, opts[:deep_check_if_needed]))
|
|
locked = {:asset_string => self.asset_string, :context_module => self.context_module_tag.context_module.attributes}
|
|
end
|
|
locked
|
|
end
|
|
end
|
|
|
|
def hidden?
|
|
self.file_state == 'hidden' || (self.folder && self.folder.hidden?)
|
|
end
|
|
memoize :hidden?
|
|
|
|
def just_hide
|
|
self.file_state == 'hidden'
|
|
end
|
|
|
|
def public?
|
|
self.file_state == 'public'
|
|
end
|
|
memoize :public?
|
|
|
|
def currently_locked
|
|
self.locked || (self.lock_at && Time.now > self.lock_at) || (self.unlock_at && Time.now < self.unlock_at) || self.file_state == 'hidden'
|
|
end
|
|
|
|
def hidden
|
|
hidden?
|
|
end
|
|
|
|
def hidden=(val)
|
|
self.file_state = (val == true || val == '1' ? 'hidden' : 'available')
|
|
end
|
|
|
|
def context_module_action(user, action)
|
|
self.context_module_tag.context_module_action(user, action) if self.context_module_tag
|
|
end
|
|
|
|
include Workflow
|
|
|
|
# Right now, using the state machine to manage whether an attachment has
|
|
# been uploaded to Scribd. It can be uploaded to other places, or
|
|
# scrubbed in other ways. All that work should be managed by the state
|
|
# machine.
|
|
workflow do
|
|
state :pending_upload do
|
|
event :upload, :transitions_to => :processing do
|
|
self.submitted_to_scribd_at = Time.now
|
|
self.scribd_attempts ||= 0
|
|
self.scribd_attempts += 1
|
|
end
|
|
event :process, :transitions_to => :processed
|
|
event :mark_errored, :transitions_to => :errored
|
|
end
|
|
|
|
state :processing do
|
|
event :process, :transitions_to => :processed
|
|
event :mark_errored, :transitions_to => :errored
|
|
end
|
|
|
|
state :processed do
|
|
event :recycle, :transitions_to => :pending_upload
|
|
end
|
|
state :errored do
|
|
event :recycle, :transitions_to => :pending_upload
|
|
end
|
|
state :to_be_zipped
|
|
state :zipping
|
|
state :zipped
|
|
state :unattached
|
|
state :unattached_temporary
|
|
end
|
|
|
|
named_scope :to_be_zipped, lambda{
|
|
{:conditions => ['attachments.workflow_state = ? AND attachments.scribd_attempts < ?', 'to_be_zipped', 10], :order => 'created_at' }
|
|
}
|
|
|
|
alias_method :destroy!, :destroy
|
|
# file_state is like workflow_state, which was already taken
|
|
# possible values are: available, deleted
|
|
def destroy(delete_media_object = true)
|
|
return if self.new_record?
|
|
self.file_state = 'deleted' #destroy
|
|
self.deleted_at = Time.now
|
|
ContentTag.delete_for(self)
|
|
MediaObject.update_all({:workflow_state => 'deleted', :updated_at => Time.now.utc}, {:attachment_id => self.id}) if self.id && delete_media_object
|
|
save!
|
|
end
|
|
|
|
def restore
|
|
self.file_state = 'active'
|
|
self.save
|
|
end
|
|
|
|
def deleted?
|
|
self.file_state == 'deleted'
|
|
end
|
|
|
|
def available?
|
|
self.file_state == 'available'
|
|
end
|
|
|
|
def scribdable?
|
|
ScribdAPI.enabled? && self.scribd_mime_type_id ? true : false
|
|
end
|
|
|
|
def self.submit_to_scribd(ids)
|
|
Attachment.find_all_by_id(ids).compact.each do |attachment|
|
|
attachment.submit_to_scribd! rescue nil
|
|
end
|
|
end
|
|
|
|
def self.skip_scribd_submits(skip=true)
|
|
@skip_scribd_submits = skip
|
|
end
|
|
|
|
def self.skip_broadcast_messages(skip=true)
|
|
@skip_broadcast_messages = skip
|
|
end
|
|
|
|
def self.skip_scribd_submits?
|
|
!!@skip_scribd_submits
|
|
end
|
|
|
|
def self.skip_media_object_creation(&block)
|
|
@skip_media_object_creation = true
|
|
block.call
|
|
ensure
|
|
@skip_media_object_creation = false
|
|
end
|
|
def self.skip_media_object_creation?
|
|
!!@skip_media_object_creation
|
|
end
|
|
|
|
# This is the engine of the Scribd machine. Submits the code to
|
|
# scribd when appropriate, otherwise adjusts the state machine. This
|
|
# should be called from another service, creating an asynchronous upload
|
|
# to Scribd. This is fairly forgiving, so that if I ask to submit
|
|
# something that shouldn't be submitted, it just returns false. If it's
|
|
# something that should never be submitted, it should just update the
|
|
# state to processed so that it doesn't try to do that again.
|
|
def submit_to_scribd!
|
|
# Newly created record that needs to be submitted to scribd
|
|
if self.pending_upload? and self.scribdable? and self.filename and ScribdAPI.enabled?
|
|
ScribdAPI.instance.set_user(self.scribd_account)
|
|
begin
|
|
upload_path = if Attachment.local_storage?
|
|
self.full_filename
|
|
else
|
|
self.authenticated_s3_url(:expires_in => 1.year)
|
|
end
|
|
self.write_attribute(:scribd_doc, ScribdAPI.upload(upload_path, self.after_extension || self.scribd_mime_type.extension))
|
|
self.cached_scribd_thumbnail = self.scribd_doc.thumbnail
|
|
self.workflow_state = 'processing'
|
|
rescue => e
|
|
self.workflow_state = 'errored'
|
|
ErrorReport.log_exception(:scribd, e, :attachment_id => self.id)
|
|
end
|
|
self.submitted_to_scribd_at = Time.now
|
|
self.scribd_attempts ||= 0
|
|
self.scribd_attempts += 1
|
|
self.save
|
|
return true
|
|
# Newly created record that isn't appropriate for scribd
|
|
elsif self.pending_upload? and not self.scribdable?
|
|
self.process!
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
def resubmit_to_scribd!
|
|
if self.scribd_doc && ScribdAPI.enabled?
|
|
ScribdAPI.instance.set_user(self.scribd_account)
|
|
self.scribd_doc.destroy rescue nil
|
|
end
|
|
self.workflow_state = 'pending_upload'
|
|
self.submit_to_scribd!
|
|
end
|
|
|
|
# Should be one of "PROCESSING", "DISPLAYABLE", "DONE", "ERROR". "DONE"
|
|
# should mean indexed, "DISPLAYABLE" is good enough for showing a user
|
|
# the iPaper. I added a state, "NOT SUBMITTED", for any attachment that
|
|
# hasn't been submitted, regardless of whether it should be. As long as
|
|
# we go through the submit_to_scribd! gateway, we'll be fine.
|
|
#
|
|
# This is a cached view of the status, it doesn't query scribd directly. That
|
|
# happens in a periodic job. Our javascript is set up to check scribd for the
|
|
# document if status is "PROCESSING" so we don't have to actually wait for
|
|
# the periodic job to find the doc is done.
|
|
def conversion_status
|
|
return 'DONE' if !ScribdAPI.enabled?
|
|
return 'ERROR' if self.errored?
|
|
if !self.scribd_doc
|
|
if !self.scribdable?
|
|
self.process
|
|
end
|
|
return 'NOT SUBMITTED'
|
|
end
|
|
return 'DONE' if self.processed?
|
|
return 'PROCESSING'
|
|
end
|
|
|
|
def query_conversion_status!
|
|
return unless ScribdAPI.enabled? && self.scribdable?
|
|
if self.scribd_doc
|
|
ScribdAPI.set_user(self.scribd_account) rescue nil
|
|
res = ScribdAPI.get_status(self.scribd_doc) rescue 'ERROR'
|
|
case res
|
|
when 'DONE'
|
|
self.process
|
|
when 'ERROR'
|
|
self.mark_errored
|
|
end
|
|
res.to_s.upcase
|
|
else
|
|
self.send_at(10.minutes.from_now, :resubmit_to_scribd!)
|
|
end
|
|
end
|
|
|
|
# Returns a link to get the document remotely.
|
|
def download_url(format='original')
|
|
return @download_url if @download_url
|
|
return nil unless ScribdAPI.enabled?
|
|
ScribdAPI.set_user(self.scribd_account)
|
|
begin
|
|
@download_url = self.scribd_doc.download_url(format)
|
|
rescue Scribd::ResponseError => e
|
|
return nil
|
|
end
|
|
end
|
|
|
|
def self.mimetype(filename)
|
|
res = nil
|
|
res = File.mime_type?(filename) if !res || res == 'unknown/unknown'
|
|
res ||= "unknown/unknown"
|
|
res
|
|
end
|
|
|
|
def mimetype(fn=nil)
|
|
res = Attachment.mimetype(filename)
|
|
res = File.mime_type?(self.uploaded_data) if (!res || res == 'unknown/unknown') && self.uploaded_data
|
|
res ||= "unknown/unknown"
|
|
res
|
|
end
|
|
|
|
def full_path
|
|
folder = (self.folder.full_name + '/') rescue Folder.root_folders(self.context).first.name + '/'
|
|
folder + self.filename
|
|
end
|
|
|
|
def matches_full_path?(path)
|
|
f_path = full_path
|
|
f_path == path || URI.unescape(f_path) == path || f_path.downcase == path.downcase || URI.unescape(f_path).downcase == path.downcase
|
|
end
|
|
|
|
def full_display_path
|
|
folder = (self.folder.full_name + '/') rescue Folder.root_folders(self.context).first.name + '/'
|
|
folder + self.display_name
|
|
end
|
|
|
|
def matches_full_display_path?(path)
|
|
fd_path = full_display_path
|
|
fd_path == path || URI.unescape(fd_path) == path || fd_path.downcase == path.downcase || URI.unescape(fd_path).downcase == path.downcase
|
|
end
|
|
|
|
def matches_filename?(match)
|
|
filename == match || display_name == match ||
|
|
URI.unescape(filename) == match || URI.unescape(display_name) == match ||
|
|
filename.downcase == match.downcase || display_name.downcase == match.downcase ||
|
|
URI.unescape(filename).downcase == match.downcase || URI.unescape(display_name).downcase == match.downcase
|
|
end
|
|
|
|
def protect_for(user)
|
|
@cant_preview_scribd_doc = !self.grants_right?(user, nil, :download)
|
|
end
|
|
|
|
def self.attachment_list_from_migration(context, ids)
|
|
return "" if !ids || !ids.is_a?(Array) || ids.empty?
|
|
description = "<h3>#{t 'title.migration_list', "Associated Files"}</h3><ul>"
|
|
ids.each do |id|
|
|
attachment = context.attachments.find_by_migration_id(id)
|
|
description += "<li><a href='/courses/#{context.id}/files/#{attachment.id}/download' class='#{'instructure_file_link' if attachment.scribdable?}'>#{attachment.display_name}</a></li>" if attachment
|
|
end
|
|
description += "</ul>";
|
|
description
|
|
end
|
|
|
|
def self.find_from_path(path, context)
|
|
list = path.split("/").select{|f| !f.empty? }
|
|
if list[0] != Folder.root_folders(context).first.name
|
|
list.unshift(Folder.root_folders(context).first.name)
|
|
end
|
|
filename = list.pop
|
|
folder = context.folder_name_lookups[list.join('/')] rescue nil
|
|
folder ||= context.folders.active.find_by_full_name(list.join('/'))
|
|
context.folder_name_lookups ||= {}
|
|
context.folder_name_lookups[list.join('/')] = folder
|
|
file = nil
|
|
if folder
|
|
file = folder.file_attachments.find_by_filename(filename)
|
|
file ||= folder.file_attachments.find_by_display_name(filename)
|
|
end
|
|
file
|
|
end
|
|
|
|
def self.domain_namespace=(val)
|
|
@@domain_namespace = val
|
|
end
|
|
|
|
def self.domain_namespace
|
|
@@domain_namespace ||= nil
|
|
end
|
|
|
|
def self.serialization_methods; [:mime_class, :scribdable?, :currently_locked]; end
|
|
cattr_accessor :skip_thumbnails
|
|
|
|
|
|
named_scope :scribdable?, :conditions => ['scribd_mime_type_id is not null']
|
|
named_scope :recyclable, :conditions => ['attachments.scribd_attempts < ? AND attachments.workflow_state = ?', 3, 'errored']
|
|
named_scope :needing_scribd_conversion_status, :conditions => ['attachments.workflow_state = ? AND attachments.updated_at < ?', 'processing', 30.minutes.ago], :limit => 50
|
|
named_scope :uploadable, :conditions => ['workflow_state = ?', 'pending_upload']
|
|
named_scope :active, :conditions => ['file_state = ?', 'available']
|
|
named_scope :thumbnailable?, :conditions => {:content_type => Technoweenie::AttachmentFu.content_types}
|
|
def self.serialization_excludes; [:uuid, :cached_s3_url, :namespace]; end
|
|
def set_serialization_options
|
|
if self.scribd_doc
|
|
@scribd_password = self.scribd_doc.secret_password
|
|
@scribd_doc_backup = self.scribd_doc.dup
|
|
@scribd_doc_backup.instance_variable_set('@attributes', self.scribd_doc.instance_variable_get('@attributes').dup)
|
|
self.scribd_doc.secret_password = ''
|
|
self.scribd_doc = nil if @cant_preview_scribd_doc
|
|
end
|
|
end
|
|
def revert_from_serialization_options
|
|
self.scribd_doc = @scribd_doc_backup
|
|
self.scribd_doc.secret_password = @scribd_password if self.scribd_doc
|
|
end
|
|
|
|
def self.process_scribd_conversion_statuses
|
|
# Runs periodically
|
|
@attachments = Attachment.needing_scribd_conversion_status
|
|
@attachments.each do |attachment|
|
|
attachment.query_conversion_status!
|
|
end
|
|
@attachments = Attachment.scribdable?.recyclable
|
|
@attachments.each do |attachment|
|
|
attachment.resubmit_to_scribd!
|
|
end
|
|
end
|
|
|
|
# returns filename, if it's already unique, or returns a modified version of
|
|
# filename that makes it unique. you can either pass existing_files as string
|
|
# filenames, in which case it'll test against those, or a block that'll be
|
|
# called repeatedly with a filename until it returns true.
|
|
def self.make_unique_filename(filename, existing_files = [], &block)
|
|
unless block
|
|
block = proc { |fname| !existing_files.include?(fname) }
|
|
end
|
|
|
|
return filename if block.call(filename)
|
|
|
|
new_name = filename
|
|
addition = 1
|
|
dir = File.dirname(filename)
|
|
dir = dir == "." ? "" : "#{dir}/"
|
|
extname = File.extname(filename)
|
|
basename = File.basename(filename, extname)
|
|
|
|
until block.call(new_name = "#{dir}#{basename}-#{addition}#{extname}")
|
|
addition += 1
|
|
end
|
|
new_name
|
|
end
|
|
end
|