░▒▓████████████████████████████████▓▒░ ░▒▓█ ▓▒░ ░▒▓█ ~ S W A M P ~ ▓▒░ ░▒▓█ ▓▒░ ░▒▓████████████████████████████████▓▒░
pplmvsvm
LOCATION:
/var/www/canvas/gems/attachment_fu/lib
☗ ROOT
↻ REFRESH
✎ CARVE FLESH
EDITING: attachment_fu.rb
# frozen_string_literal: true # # Copyright (C) 2014 - present 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/>. require "attachment_fu/railtie" require "attachment_fu/processors/mini_magick_processor" require "attachment_fu/backends/file_system_backend" require "attachment_fu/backends/s3_backend" module AttachmentFu # :nodoc: @@default_processors = %w[MiniMagick] # Instructure: I (ryan shaw) just copied and pasted this from http://github.com/technoweenie/attachment_fu/blob/master/lib/technoweenie/attachment_fu.rb @@content_types = [ "image/jpeg", "image/pjpeg", "image/jpg", "image/gif", "image/png", "image/x-png", "image/jpg", "image/x-ms-bmp", "image/bmp", "image/x-bmp", "image/webp", "image/x-bitmap", "image/x-xbitmap", "image/x-win-bitmap", "image/x-windows-bmp", "image/ms-bmp", "application/bmp", "application/x-bmp", "application/x-win-bitmap", "application/preview", "image/jp_", "application/jpg", "application/x-jpg", "image/pipeg", "image/vnd.swiftview-jpeg", "image/x-xbitmap", "application/png", "application/x-png", "image/gi_", "image/x-citrix-pjpeg" ] mattr_reader :content_types, :tempfile_path, :default_processors mattr_writer :tempfile_path class ThumbnailError < StandardError; end class AttachmentError < StandardError; end module ActMethods # Options: # * <tt>:content_type</tt> - Allowed content types. Allows all by default. Use :image to allow all standard image types. # * <tt>:min_size</tt> - Minimum size allowed. 1 byte is the default. # * <tt>:max_size</tt> - Maximum size allowed. 1.megabyte is the default. # * <tt>:size</tt> - Range of sizes allowed. (1..1.megabyte) is the default. This overrides the :min_size and :max_size options. # * <tt>:resize_to</tt> - Used by RMagick to resize images. Pass either an array of width/height, or a geometry string. # * <tt>:thumbnails</tt> - Specifies a set of thumbnails to generate. This accepts a hash of filename suffixes and RMagick resizing options. # * <tt>:thumbnail_class</tt> - Set what class to use for thumbnails. This attachment class is used by default. # * <tt>:path_prefix</tt> - path to store the uploaded files. Uses public/#{table_name} by default for the filesystem, and just #{table_name} # for the S3 backend. Setting this sets the :storage to :file_system. # * <tt>:storage</tt> - Use :file_system to specify the attachment data is stored with the file system. Defaults to :db_system. # * <tt>:keep_profile</tt> By default image EXIF data will be stripped to minimize image size. For small thumbnails this proivides important savings. Picture quality is not affected. Set to false if you want to keep the image profile as is. ImageScience will allways keep EXIF data. # # Examples: # has_attachment :max_size => 1.kilobyte # has_attachment :size => 1.megabyte..2.megabytes # has_attachment :content_type => 'application/pdf' # has_attachment :content_type => ['application/pdf', 'application/msword', 'text/plain'] # has_attachment :content_type => :image, :resize_to => [50,50] # has_attachment :content_type => ['application/pdf', :image], :resize_to => 'x50' # has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' } # has_attachment :storage => :file_system, :path_prefix => 'public/files' # has_attachment :storage => :file_system, :path_prefix => 'public/files', # :content_type => :image, :resize_to => [50,50] # has_attachment :storage => :file_system, :path_prefix => 'public/files', # :thumbnails => { :thumb => [50, 50], :geometry => 'x50' } # has_attachment :storage => :s3 def has_attachment(options = {}) # this allows you to redefine the acts' options for each subclass, however options[:min_size] ||= 1 options[:max_size] ||= 1.megabyte options[:size] ||= (options[:min_size]..options[:max_size]) options[:thumbnails] ||= {} options[:thumbnail_class] ||= self options[:s3_access] ||= "public-read" options[:content_type] = [options[:content_type]].flatten.collect! { |t| (t == :image) ? AttachmentFu.content_types : t }.flatten unless options[:content_type].nil? unless options[:thumbnails].is_a?(Hash) raise ArgumentError, ":thumbnails option should be a hash: e.g. :thumbnails => { :foo => '50x50' }" end extend ClassMethods unless (class << self; included_modules; end).include?(ClassMethods) include InstanceMethods unless included_modules.include?(InstanceMethods) parent_options = attachment_options || {} # doing these shenanigans so that #attachment_options is available to processors and backends self.attachment_options = options attr_accessor :thumbnail_resize_options attachment_options[:storage] ||= (attachment_options[:file_system_path] || attachment_options[:path_prefix]) ? :file_system : :db_file attachment_options[:storage] ||= parent_options[:storage] attachment_options[:path_prefix] ||= attachment_options[:file_system_path] if attachment_options[:path_prefix].nil? attachment_options[:path_prefix] = (attachment_options[:storage] == :s3) ? table_name : File.join("public", table_name) end attachment_options[:path_prefix] = attachment_options[:path_prefix][1..] if options[:path_prefix].first == "/" with_options foreign_key: "parent_id" do |m| m.has_many :thumbnails, class_name: "::#{attachment_options[:thumbnail_class]}" m.belongs_to :parent, class_name: "::#{base_class}" unless options[:thumbnails].empty? end storage_mod = AttachmentFu::Backends.const_get("#{options[:storage].to_s.classify}Backend") include storage_mod unless included_modules.include?(storage_mod) unless parent_options[:processor] case attachment_options[:processor] when :none, nil processors = AttachmentFu.default_processors.dup begin if processors.any? attachment_options[:processor] = "#{processors.first}Processor" processor_mod = AttachmentFu::Processors.const_get(attachment_options[:processor]) prepend processor_mod unless included_modules.include?(processor_mod) end rescue Object raise unless load_related_exception?($!) processors.shift retry end else begin processor_mod = AttachmentFu::Processors.const_get("#{attachment_options[:processor].to_s.classify}Processor") include processor_mod unless included_modules.include?(processor_mod) rescue Object raise unless load_related_exception?($!) puts "Problems loading #{options[:processor]}Processor: #{$!}" end end end # Don't let child override processor end def load_related_exception?(e) # :nodoc: implementation specific # We can't rescue CompilationError directly, as it is part of the RubyInline library. # We must instead rescue RuntimeError, and check the class' name. e.is_a?(LoadError) || e.is_a?(MissingSourceFile) || e.instance_of?(CompilationError) end private :load_related_exception? end module ClassMethods delegate :content_types, to: AttachmentFu # Performs common validations for attachment models. def validates_as_attachment validates_presence_of :size, :content_type, :filename validate :attachment_attributes_valid? end # Returns true or false if the given content type is recognized as an image. def image?(content_type) content_types.include?(content_type) end def self.extended(base) base.class_attribute :attachment_options, instance_reader: false, instance_writer: false base.before_destroy :destroy_thumbnails base.before_validation :set_size_from_temp_path base.after_save :after_process_attachment base.after_destroy :destroy_file base.after_validation :process_attachment base.define_model_callbacks :save_and_attachment_processing, only: [:after] end # Get the thumbnail class, which is the current attachment class by default. # Configure this with the :thumbnail_class option. def thumbnail_class attachment_options[:thumbnail_class] = attachment_options[:thumbnail_class].constantize unless attachment_options[:thumbnail_class].is_a?(Class) attachment_options[:thumbnail_class] end # Copies the given file path to a new tempfile, returning the closed tempfile. def copy_to_temp_file(file, temp_base_name) Tempfile.new(["", temp_base_name], AttachmentFu.tempfile_path).tap do |tmp| tmp.close FileUtils.cp file, tmp.path end end # Writes the given data to a new tempfile, returning the closed tempfile. def write_to_temp_file(data, temp_base_name) Tempfile.new(["", temp_base_name], AttachmentFu.tempfile_path).tap do |tmp| tmp.binmode tmp.write data tmp.close end end end module InstanceMethods require "rack" def attachment_options @attachment_options || self.class.attachment_options end def attachment_options=(value) @attachment_options = self.class.attachment_options.merge(value) end # Checks whether the attachment's content type is an image content type def image? self.class.image?(content_type) end # Returns true/false if an attachment is thumbnailable. A thumbnailable attachment has an image content type and the parent_id attribute. def thumbnailable? image? && respond_to?(:parent_id) && parent_id.nil? end # Returns the class used to create new thumbnails for this attachment. def thumbnail_class self.class.thumbnail_class end # Gets the thumbnail name for a filename. 'foo.jpg' becomes 'foo_thumbnail.jpg' def thumbnail_name_for(thumbnail = nil) return filename if thumbnail.blank? ext = nil basename = filename.gsub(/\.\w+$/) do |s| ext = s "" end # ImageScience doesn't create gif thumbnails, only pngs ext.sub!(/gif$/, "png") if attachment_options[:processor] == "ImageScience" name = "#{basename}_#{thumbnail}#{ext}" if name.length > 255 name = "#{basename[0..(254 - name.length)]}_#{thumbnail}#{ext}" end name end # Creates or updates the thumbnail for the current attachment. def create_or_update_thumbnail(temp_file, file_name_suffix, *size) thumbnailable? || raise(ThumbnailError, "Can't create a thumbnail if the content type is not an image or there is no parent_id column") find_or_initialize_thumbnail(file_name_suffix).tap do |thumb| thumb.attributes = { content_type:, filename: thumbnail_name_for(file_name_suffix), temp_path: temp_file, thumbnail_resize_options: size } if thumb.valid? thumb.process_attachment thumb.save! end end end def create_thumbnail_size(target_size) actual_size = attachment_options[:thumbnails][target_size] raise "this class doesn't have a thubnail size for #{target_size}" if actual_size.nil? begin tmp = create_temp_file res = create_or_update_thumbnail(tmp, target_size.to_s, actual_size) rescue Aws::S3::Errors::NoSuchKey => e logger.warn("error when trying to make thumbnail for attachment_id: #{id} (the image probably doesn't exist on s3) error details: #{e.inspect}") rescue ThumbnailError => e logger.warn("error creating thumbnail for attachment_id #{id}: #{e.inspect}") ensure tmp&.unlink end res end # Sets the content type. def content_type=(new_type) write_attribute :content_type, new_type.to_s.strip end # Sanitizes a filename. def filename=(new_name) write_attribute :filename, sanitize_filename(new_name) end # Returns the width/height in a suitable format for the image_tag helper: (100x100) def image_size [width.to_s, height.to_s].join("x") end # Returns true if the attachment data will be written to the storage system on the next save # This only works if temp_path is set by something else, which happens in the local file # system and with s3 files when creating an image thumbnail (but not with regular s3 file uploads # or instfs uploads of any kind) def save_attachment_from_temp_path? if is_a?(Attachment) if root_attachment_id && new_record? return false end filename && File.file?(temp_path.to_s) else File.file?(temp_path.to_s) end end # nil placeholder in case this field is used in a form. def uploaded_data nil end # This method handles the uploaded file object. If you set the field name to uploaded_data, you don't need # any special code in your controller. # # <% form_for :attachment, :html => { :multipart => true } do |f| -%> # <p><%= f.file_field :uploaded_data %></p> # <p><%= submit_tag :Save %> # <% end -%> # # @attachment = Attachment.create! params[:attachment] # # TODO: Allow it to work with Merb tempfiles too. def uploaded_data=(file_data) return if file_data.blank? if is_a?(Attachment) # glean information from the file handle self.content_type = File.mime_types.include?(content_type) ? content_type : detect_mimetype(file_data) self.filename = file_data.original_filename if respond_to?(:filename) && file_data.respond_to?(:original_filename) file_from_path = true if file_data.respond_to?(:path) && file_data.path.present? temp_paths.unshift file_data else file_data.rewind self.temp_data = file_data.read file_from_path = false end # If we're overwriting an existing file, we need to take serious # precautions, since other Attachment records could be using this file. # We first remove any root references for this file, and then we generate # a new unique filename for this file so any children of this attachment # will still be able to get at the original. unless new_record? self.root_attachment = nil self.root_attachment_id = nil self.workflow_state = nil self.filename = filename.sub(/\A\d+_\d+__/, "") self.filename = "#{Time.now.to_i}_#{rand(999)}__#{filename}" if filename end unless attachment_options[:skip_sis] read_bytes = false digest = Digest::MD5.new begin io = file_data if file_from_path io = File.open(temp_path, "rb") end io.rewind io.each_line do |line| digest.update(line) read_bytes = true end rescue nil ensure io.close if file_from_path end end self.md5 = read_bytes ? digest.hexdigest : nil if (existing_attachment = find_existing_attachment_for_md5) self.temp_path = nil if respond_to?(:temp_path=) self.temp_data = nil if respond_to?(:temp_data=) write_attribute(:filename, nil) if respond_to?(:filename=) self.root_attachment = existing_attachment end file_data else self.content_type = file_data.content_type self.filename = file_data.original_filename if respond_to?(:filename) if file_data.respond_to?(:path) self.temp_path = file_data else file_data.rewind self.temp_data = file_data.read end end end def find_existing_attachment_for_md5 shard.activate do GuardRail.activate(:secondary) do if md5.present? && (ns = infer_namespace) scope = Attachment.where(md5:, namespace: ns, root_attachment_id: nil, content_type:) scope = scope.where.not(filename: nil) scope = scope.where.not(id: self) unless new_record? scope.detect { |a| a.store.exists? } end end end end def detect_mimetype(file_data) if file_data.respond_to?(:content_type) && (file_data.content_type.blank? || file_data.content_type.strip == "application/octet-stream") res = nil res ||= File.mime_type?(file_data.original_filename) if file_data.respond_to?(:original_filename) res ||= File.mime_type?(file_data) res ||= "text/plain" unless file_data.respond_to?(:path) res || "unknown/unknown" elsif file_data.respond_to?(:content_type) file_data.content_type else File.mime_type?(file_data) end end # Gets the latest temp path from the collection of temp paths. While working with an attachment, # multiple Tempfile objects may be created for various processing purposes (resizing, for example). # An array of all the tempfile objects is stored so that the Tempfile instance is held on to until # it's not needed anymore. The collection is cleared after saving the attachment. def temp_path p = temp_paths.first p.respond_to?(:path) ? p.path : p.to_s end # Gets an array of the currently used temp paths. Defaults to a copy of #full_filename. def temp_paths # INSTRUCTURE: was "@temp_paths ||= (new_record? || !respond_to?(:full_filename) || !File.exist?(full_filename) ?" @temp_paths ||= if new_record? || !respond_to?(:full_filename) || !full_filename || !File.exist?(full_filename) [] else [copy_to_temp_file(full_filename)] end end # Adds a new temp_path to the array. This should take a string or a Tempfile. This class makes no # attempt to remove the files, so Tempfiles should be used. Tempfiles remove themselves when they go out of scope. # You can also use string paths for temporary files, such as those used for uploaded files in a web server. def temp_path=(value) temp_paths.unshift value temp_path end # Gets the data from the latest temp file. This will read the file into memory. def temp_data save_attachment_from_temp_path? ? File.read(temp_path) : nil end # Writes the given data to a Tempfile and adds it to the collection of temp files. def temp_data=(data) self.temp_path = write_to_temp_file data unless data.nil? end # Copies the given file to a randomly named Tempfile. def copy_to_temp_file(file) self.class.copy_to_temp_file file, random_tempfile_filename end # Writes the given file to a randomly named Tempfile. def write_to_temp_file(data) self.class.write_to_temp_file data, random_tempfile_filename end # Stub for creating a temp file from the attachment data. This should be defined in the backend module. def create_temp_file() end # Allows you to work with a processed representation (RMagick, ImageScience, etc) of the attachment in a block. # # @attachment.with_image do |img| # self.data = img.thumbnail(100, 100).to_blob # end # def with_image(&) self.class.with_image(temp_path, &) end protected # Generates a unique filename for a Tempfile. def random_tempfile_filename "#{rand Time.now.to_i}#{filename&.last(50) || "attachment"}" end def sanitize_filename(filename) filename.strip.tap do |name| # NOTE: File.basename doesn't work right with Windows paths on Unix # get only the filename, not the whole path name.gsub!(%r{^.*(\\|/)}, "") # Finally, replace all non alphanumeric, underscore or periods with underscore name.gsub!(/[^\w.-]/, "_") end end # before_validation callback. def set_size_from_temp_path self.size = File.size(temp_path) if save_attachment_from_temp_path? end # validates the size and content_type attributes according to the current model's options def attachment_attributes_valid? [:size, :content_type].each do |attr_name| enum = attachment_options[attr_name] errors.add attr_name, ActiveRecord::Errors.default_error_messages[:inclusion] unless enum.nil? || enum.include?(send(attr_name)) end end # Initializes a new thumbnail with the given suffix. def find_or_initialize_thumbnail(file_name_suffix) scope = thumbnail_class.where(thumbnail: file_name_suffix.to_s) scope = scope.where(parent_id: id) if respond_to?(:parent_id) scope.first_or_initialize end # Stub for a #process_attachment method in a processor def process_attachment @saved_attachment = save_attachment_from_temp_path? run_before_attachment_saved if @saved_attachment && respond_to?(:run_before_attachment_saved) @saved_attachment end # Cleans up after processing. Thumbnails are created, the attachment is stored to the backend, and the temp_paths are cleared. def after_process_attachment if @saved_attachment # # INSTRUCTURE I (ryan shaw) commented these next lines out so that the thumbnailing does not happen syncronisly as part of the request. # # we are going to do the same thing as delayed_jobs # if respond_to?(:process_attachment) && thumbnailable? && !attachment_options[:thumbnails].blank? && parent_id.nil? # temp_file = temp_path || create_temp_file # attachment_options[:thumbnails].each { |suffix, size| create_or_update_thumbnail(temp_file, suffix, *size) } # end # In normal attachment upload usage, the only transaction we should # be inside is the AR#save transaction. If that's the case, defer # the upload and callbacks until after the transaction commits. If # the upload fails, that will leave this attachment in an # unattached state, but that's already the case in other error # situations as well. # # If there is no transaction, or more than one transaction, then # just upload immediately. This can happen if # after_process_attachment is called directly, or if we're inside # an rspec test run (which is wrapped in an outer transaction). # It can also happen if somebody explicitly uploads file data # inside a .transaction block, which we normally shouldn't do. save_and_callbacks = proc do save_to_storage @temp_paths.clear @saved_attachment = nil run_after_attachment_saved if respond_to?(:run_after_attachment_saved) run_callbacks(:save_and_attachment_processing) end if Rails.env.test? save_and_callbacks.call else self.class.connection.after_transaction_commit(&save_and_callbacks) end else run_callbacks(:save_and_attachment_processing) end end # Resizes the given processed img object with either the attachment resize options or the thumbnail resize options. def resize_image_or_thumbnail!(img) if (!respond_to?(:parent_id) || parent_id.nil?) && attachment_options[:resize_to] # parent image resize_image(img, attachment_options[:resize_to]) elsif thumbnail_resize_options # thumbnail resize_image(img, thumbnail_resize_options) end end # Removes the thumbnails for the attachment, if it has any def destroy_thumbnails thumbnails.each(&:destroy) if thumbnailable? end end end # backwards-compatible shim module Technoweenie AttachmentFu = ::AttachmentFu end
CANCEL
Name
Type
Size
Modified
Actions
↩ ..
DIR
—
—
📁 attachment_fu/
DIR
—
2023-09-24 03:36
📄 attachment_fu.rb
RB
24.2 KB
2023-09-24 03:36
EDIT