2013-08-01 10:33:49 +08:00
# Redmine - project management software
# Copyright (C) 2006-2013 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
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require " digest/md5 "
require " fileutils "
class Attachment < ActiveRecord :: Base
belongs_to :container , :polymorphic = > true
2014-01-13 21:02:45 +08:00
belongs_to :project , foreign_key : 'container_id' , conditions : " attachments.container_type = 'Project' "
2014-04-09 09:25:24 +08:00
belongs_to :softapplication , foreign_key : 'container_id' , conditions : " attachments.container_type = 'Softapplication' "
2013-08-01 10:33:49 +08:00
belongs_to :author , :class_name = > " User " , :foreign_key = > " author_id "
2014-05-12 18:46:39 +08:00
belongs_to :attachmentstype , :foreign_key = > " attachtype " , :primary_key = > " id "
2013-08-01 10:33:49 +08:00
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
2014-03-01 10:14:01 +08:00
acts_as_taggable
2013-08-01 10:33:49 +08:00
acts_as_event :title = > :filename ,
:url = > Proc . new { | o | { :controller = > 'attachments' , :action = > 'download' , :id = > o . id , :filename = > o . filename } }
2014-04-09 09:25:24 +08:00
acts_as_activity_provider :type = > 'files' ,
2013-08-01 10:33:49 +08:00
:permission = > :view_files ,
:author_key = > :author_id ,
:find_options = > { :select = > " #{ Attachment . table_name } .* " ,
:joins = > " LEFT JOIN #{ Version . table_name } ON #{ Attachment . table_name } .container_type='Version' AND #{ Version . table_name } .id = #{ Attachment . table_name } .container_id " +
" LEFT JOIN #{ Project . table_name } ON #{ Version . table_name } .project_id = #{ Project . table_name } .id OR ( #{ Attachment . table_name } .container_type='Project' AND #{ Attachment . table_name } .container_id = #{ Project . table_name } .id ) " }
acts_as_activity_provider :type = > 'documents' ,
:permission = > :view_documents ,
:author_key = > :author_id ,
:find_options = > { :select = > " #{ Attachment . table_name } .* " ,
:joins = > " LEFT JOIN #{ Document . table_name } ON #{ Attachment . table_name } .container_type='Document' AND #{ Document . table_name } .id = #{ Attachment . table_name } .container_id " +
" 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' ] || 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 @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
def file = ( incoming_file )
unless incoming_file . nil?
@temp_file = incoming_file
if @temp_file . size > 0
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 )
filename
end
# Copies the temporary file to its final location
# and computes its MD5 hash
def files_to_final_location
if @temp_file && ( @temp_file . size > 0 )
self . disk_directory = target_directory
self . disk_filename = Attachment . disk_filename ( filename , disk_directory )
logger . info ( " Saving attachment ' #{ self . diskfile } ' ( #{ @temp_file . size } bytes) " )
path = File . dirname ( diskfile )
unless File . directory? ( path )
FileUtils . mkdir_p ( path )
end
md5 = Digest :: MD5 . new
File . open ( diskfile , " wb " ) do | f |
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
end
@temp_file = nil
# Don't save the content type if it's longer than the authorized length
if self . content_type && self . content_type . length > 255
self . content_type = nil
end
end
# Deletes the file from the file system if it's not referenced by other attachments
def delete_from_disk
if Attachment . where ( " disk_filename = ? AND id <> ? " , disk_filename , id ) . empty?
delete_from_disk!
end
end
# Returns file's location on disk
def diskfile
File . join ( self . class . storage_path , disk_directory . to_s , disk_filename . to_s )
end
def title
title = filename . to_s
if description . present?
title << " ( #{ description } ) "
end
title
end
def increment_download
increment! ( :downloads )
end
def project
container . try ( :project )
end
def visible? ( user = User . current )
if container_id
container && container . attachments_visible? ( user )
else
author == user
end
end
def deletable? ( user = User . current )
if container_id
2014-03-05 11:17:16 +08:00
container && ( container . is_a? ( Project ) ? container . attachments_deletable? ( user ) : true )
2013-08-01 10:33:49 +08:00
else
author == user
end
end
def image?
! ! ( self . filename =~ / \ .(bmp|gif|jpg|jpe|jpeg|png)$ /i )
end
2014-04-14 15:54:10 +08:00
def pack?
2014-04-23 16:30:23 +08:00
! ! ( self . filename =~ / \ .(zip|rar|tar|gz|exe|jar|7z|iso)$ /i )
2014-04-14 15:54:10 +08:00
end
2013-08-01 10:33:49 +08:00
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 } \n Exception 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?
Redmine :: MimeType . is_type? ( 'text' , filename )
end
def is_diff?
self . filename =~ / \ .(patch|diff)$ /i
end
# Returns true if the file is readable
def readable?
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 )
result = obj . save_attachments ( attachments , User . current )
obj . attach_saved_attachments
result
end
2014-05-09 15:45:21 +08:00
def self . attach_filesex ( obj , attachments , attachment_type )
result = obj . save_attachmentsex ( attachments , User . current , attachment_type )
obj . attach_saved_attachments
result
end
2013-08-01 10:33:49 +08:00
def self . latest_attach ( attachments , filename )
attachments . sort_by ( & :created_on ) . reverse . detect {
| att | att . filename . downcase == filename . downcase
}
end
def self . prune ( age = 1 . day )
Attachment . where ( " created_on < ? AND (container_type IS NULL OR container_type = '') " , Time . now - age ) . destroy_all
end
# Moves an existing attachment to its target directory
def move_to_target_directory!
if ! new_record? & readable?
src = diskfile
self . disk_directory = target_directory
dest = diskfile
if src != dest && FileUtils . mkdir_p ( File . dirname ( dest ) ) && FileUtils . mv ( src , dest )
update_column :disk_directory , disk_directory
end
end
end
# Moves existing attachments that are stored at the root of the files
# directory (ie. created before Redmine 2.3) to their target subdirectories
def self . move_from_root_to_target_directory
Attachment . where ( " disk_directory IS NULL OR disk_directory = '' " ) . find_each do | attachment |
attachment . move_to_target_directory!
end
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 ( / ^.*( \\ | \/ ) / , '' )
# Finally, replace invalid characters with underscore
@filename = just_filename . gsub ( / [ \/ \ ? \ % \ * \ : \ | \ " \ '<>]+ / , '_' )
end
# Returns the subdirectory in which the attachment will be saved
def target_directory
time = created_on || DateTime . now
time . strftime ( " %Y/%m " )
end
# Returns an ASCII or hashed filename that do not
# exists yet in the given subdirectory
def self . disk_filename ( filename , directory = nil )
timestamp = DateTime . now . strftime ( " %y%m%d%H%M%S " )
ascii = ''
if filename =~ %r{ ^[a-zA-Z0-9_ \ . \ -]*$ }
ascii = filename
else
ascii = Digest :: MD5 . hexdigest ( filename )
# keep the extension if any
ascii << $1 if filename =~ %r{ ( \ .[a-zA-Z0-9]+)$ }
end
while File . exist? ( File . join ( storage_path , directory . to_s , " #{ timestamp } _ #{ ascii } " ) )
timestamp . succ!
end
" #{ timestamp } _ #{ ascii } "
end
end