diff app/models/attachment.rb @ 1338:25603efa57b5

Merge from live branch
author Chris Cannam
date Thu, 20 Jun 2013 13:14:14 +0100
parents 433d4f72a19b
children 622f24f53b42 261b3d9a4903
line wrap: on
line diff
--- a/app/models/attachment.rb	Wed Jan 23 13:11:25 2013 +0000
+++ b/app/models/attachment.rb	Thu Jun 20 13:14:14 2013 +0100
@@ -1,5 +1,5 @@
 # Redmine - project management software
-# Copyright (C) 2006-2011  Jean-Philippe Lang
+# Copyright (C) 2006-2012  Jean-Philippe Lang
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -21,9 +21,10 @@
   belongs_to :container, :polymorphic => true
   belongs_to :author, :class_name => "User", :foreign_key => "author_id"
 
-  validates_presence_of :container, :filename, :author
+  validates_presence_of :filename, :author
   validates_length_of :filename, :maximum => 255
   validates_length_of :disk_filename, :maximum => 255
+  validates_length_of :description, :maximum => 255
   validate :validate_max_file_size
 
   acts_as_event :title => :filename,
@@ -44,14 +45,25 @@
                                                         "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
 
   cattr_accessor :storage_path
-  @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
+  @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
+
+  cattr_accessor :thumbnails_storage_path
+  @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
 
   before_save :files_to_final_location
   after_destroy :delete_from_disk
 
+  # Returns an unsaved copy of the attachment
+  def copy(attributes=nil)
+    copy = self.class.new
+    copy.attributes = self.attributes.dup.except("id", "downloads")
+    copy.attributes = attributes if attributes
+    copy
+  end
+
   def validate_max_file_size
-    if self.filesize > Setting.attachment_max_size.to_i.kilobytes
-      errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
+    if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
+      errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
     end
   end
 
@@ -59,21 +71,33 @@
     unless incoming_file.nil?
       @temp_file = incoming_file
       if @temp_file.size > 0
-        self.filename = sanitize_filename(@temp_file.original_filename)
-        self.disk_filename = Attachment.disk_filename(filename)
-        self.content_type = @temp_file.content_type.to_s.chomp
-        if content_type.blank?
+        if @temp_file.respond_to?(:original_filename)
+          self.filename = @temp_file.original_filename
+          self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
+        end
+        if @temp_file.respond_to?(:content_type)
+          self.content_type = @temp_file.content_type.to_s.chomp
+        end
+        if content_type.blank? && filename.present?
           self.content_type = Redmine::MimeType.of(filename)
         end
         self.filesize = @temp_file.size
       end
     end
   end
-	
+
   def file
     nil
   end
 
+  def filename=(arg)
+    write_attribute :filename, sanitize_filename(arg.to_s)
+    if new_record? && disk_filename.blank?
+      self.disk_filename = Attachment.disk_filename(filename)
+    end
+    filename
+  end
+
   # Copies the temporary file to its final location
   # and computes its MD5 hash
   def files_to_final_location
@@ -81,10 +105,15 @@
       logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
       md5 = Digest::MD5.new
       File.open(diskfile, "wb") do |f|
-        buffer = ""
-        while (buffer = @temp_file.read(8192))
-          f.write(buffer)
-          md5.update(buffer)
+        if @temp_file.respond_to?(:read)
+          buffer = ""
+          while (buffer = @temp_file.read(8192))
+            f.write(buffer)
+            md5.update(buffer)
+          end
+        else
+          f.write(@temp_file)
+          md5.update(@temp_file)
         end
       end
       self.digest = md5.hexdigest
@@ -96,14 +125,24 @@
     end
   end
 
-  # Deletes file on the disk
+  # Deletes the file from the file system if it's not referenced by other attachments
   def delete_from_disk
-    File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
+    if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
+      delete_from_disk!
+    end
   end
 
   # Returns file's location on disk
   def diskfile
-    "#{@@storage_path}/#{self.disk_filename}"
+    File.join(self.class.storage_path, disk_filename.to_s)
+  end
+
+  def title
+    title = filename.to_s
+    if description.present?
+      title << " (#{description})"
+    end
+    title
   end
 
   def increment_download
@@ -111,19 +150,55 @@
   end
 
   def project
-    container.project
+    container.try(:project)
   end
 
   def visible?(user=User.current)
-    container.attachments_visible?(user)
+    container && container.attachments_visible?(user)
   end
 
   def deletable?(user=User.current)
-    container.attachments_deletable?(user)
+    container && container.attachments_deletable?(user)
   end
 
   def image?
-    self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
+    !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
+  end
+
+  def thumbnailable?
+    image?
+  end
+
+  # Returns the full path the attachment thumbnail, or nil
+  # if the thumbnail cannot be generated.
+  def thumbnail(options={})
+    if thumbnailable? && readable?
+      size = options[:size].to_i
+      if size > 0
+        # Limit the number of thumbnails per image
+        size = (size / 50) * 50
+        # Maximum thumbnail size
+        size = 800 if size > 800
+      else
+        size = Setting.thumbnails_size.to_i
+      end
+      size = 100 unless size > 0
+      target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
+
+      begin
+        Redmine::Thumbnail.generate(self.diskfile, target, size)
+      rescue => e
+        logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
+        return nil
+      end
+    end
+  end
+
+  # Deletes all thumbnails
+  def self.clear_thumbnails
+    Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
+      File.delete file
+    end
   end
 
   def is_text?
@@ -139,41 +214,52 @@
     File.readable?(diskfile)
   end
 
+  # Returns the attachment token
+  def token
+    "#{id}.#{digest}"
+  end
+
+  # Finds an attachment that matches the given token and that has no container
+  def self.find_by_token(token)
+    if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
+      attachment_id, attachment_digest = $1, $2
+      attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
+      if attachment && attachment.container.nil?
+        attachment
+      end
+    end
+  end
+
   # Bulk attaches a set of files to an object
   #
   # Returns a Hash of the results:
   # :files => array of the attached files
   # :unsaved => array of the files that could not be attached
   def self.attach_files(obj, attachments)
-    attached = []
-    if attachments && attachments.is_a?(Hash)
-      attachments.each_value do |attachment|
-        file = attachment['file']
-        next unless file && file.size > 0
-        a = Attachment.create(:container => obj,
-                              :file => file,
-                              :description => attachment['description'].to_s.strip,
-                              :author => User.current)
-        obj.attachments << a
-
-        if a.new_record?
-          obj.unsaved_attachments ||= []
-          obj.unsaved_attachments << a
-        else
-          attached << a
-        end
-      end
-    end
-    {:files => attached, :unsaved => obj.unsaved_attachments}
+    result = obj.save_attachments(attachments, User.current)
+    obj.attach_saved_attachments
+    result
   end
 
   def self.latest_attach(attachments, filename)
-    attachments.sort_by(&:created_on).reverse.detect { 
+    attachments.sort_by(&:created_on).reverse.detect {
       |att| att.filename.downcase == filename.downcase
      }
   end
 
-private
+  def self.prune(age=1.day)
+    Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
+  end
+
+  private
+
+  # Physically deletes the file from the file system
+  def delete_from_disk!
+    if disk_filename.present? && File.exist?(diskfile)
+      File.delete(diskfile)
+    end
+  end
+
   def sanitize_filename(value)
     # get only the filename, not the whole path
     just_filename = value.gsub(/^.*(\\|\/)/, '')