2011-05-16 03:45:13 +04:00
# Redmine - project management software
2013-01-12 13:29:31 +04:00
# Copyright (C) 2006-2013 Jean-Philippe Lang
2007-03-12 20:59:02 +03:00
#
# 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.
2011-05-16 03:45:13 +04:00
#
2007-03-12 20:59:02 +03:00
# 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.
2011-05-16 03:45:13 +04:00
#
2007-03-12 20:59:02 +03:00
# 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 "
2012-12-13 16:07:19 +04:00
require " fileutils "
2007-03-12 20:59:02 +03:00
class Attachment < ActiveRecord :: Base
belongs_to :container , :polymorphic = > true
belongs_to :author , :class_name = > " User " , :foreign_key = > " author_id "
2011-05-16 03:45:13 +04:00
2012-02-17 01:00:11 +04:00
validates_presence_of :filename , :author
2007-07-16 21:16:49 +04:00
validates_length_of :filename , :maximum = > 255
validates_length_of :disk_filename , :maximum = > 255
2012-07-15 18:42:00 +04:00
validates_length_of :description , :maximum = > 255
2011-08-29 06:32:18 +04:00
validate :validate_max_file_size
2007-08-29 20:52:35 +04:00
acts_as_event :title = > :filename ,
2008-07-22 21:55:19 +04:00
:url = > Proc . new { | o | { :controller = > 'attachments' , :action = > 'download' , :id = > o . id , :filename = > o . filename } }
2007-08-29 20:52:35 +04:00
2008-07-27 21:54:09 +04:00
acts_as_activity_provider :type = > 'files' ,
:permission = > :view_files ,
2008-11-30 14:18:22 +03:00
:author_key = > :author_id ,
2011-05-16 03:45:13 +04:00
:find_options = > { :select = > " #{ Attachment . table_name } .* " ,
2008-07-27 21:54:09 +04:00
:joins = > " LEFT JOIN #{ Version . table_name } ON #{ Attachment . table_name } .container_type='Version' AND #{ Version . table_name } .id = #{ Attachment . table_name } .container_id " +
2009-03-08 17:31:15 +03:00
" 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 ) " }
2011-05-16 03:45:13 +04:00
2008-07-27 21:54:09 +04:00
acts_as_activity_provider :type = > 'documents' ,
:permission = > :view_documents ,
2008-11-30 14:18:22 +03:00
:author_key = > :author_id ,
2011-05-16 03:45:13 +04:00
:find_options = > { :select = > " #{ Attachment . table_name } .* " ,
2008-07-27 21:54:09 +04:00
: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 " }
2007-03-12 20:59:02 +03:00
cattr_accessor :storage_path
2012-04-28 15:20:13 +04:00
@@storage_path = Redmine :: Configuration [ 'attachments_storage_path' ] || File . join ( Rails . root , " files " )
2011-05-16 03:45:13 +04:00
2012-07-07 17:48:07 +04:00
cattr_accessor :thumbnails_storage_path
@@thumbnails_storage_path = File . join ( Rails . root , " tmp " , " thumbnails " )
2011-08-30 08:11:48 +04:00
before_save :files_to_final_location
2011-08-30 09:08:21 +04:00
after_destroy :delete_from_disk
2011-08-30 08:11:48 +04:00
2012-01-20 21:56:28 +04:00
# 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
2011-08-29 06:32:18 +04:00
def validate_max_file_size
2012-01-20 21:56:28 +04:00
if @temp_file && self . filesize > Setting . attachment_max_size . to_i . kilobytes
2012-02-23 16:18:10 +04:00
errors . add ( :base , l ( :error_attachment_too_big , :max_size = > Setting . attachment_max_size . to_i . kilobytes ) )
2009-04-08 20:56:01 +04:00
end
2007-03-12 20:59:02 +03:00
end
2008-04-03 01:30:32 +04:00
def file = ( incoming_file )
unless incoming_file . nil?
@temp_file = incoming_file
if @temp_file . size > 0
2012-02-23 14:01:16 +04:00
if @temp_file . respond_to? ( :original_filename )
self . filename = @temp_file . original_filename
2012-04-06 13:31:29 +04:00
self . filename . force_encoding ( " UTF-8 " ) if filename . respond_to? ( :force_encoding )
2012-02-23 14:01:16 +04:00
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?
2009-12-29 16:28:30 +03:00
self . content_type = Redmine :: MimeType . of ( filename )
end
2008-04-03 01:30:32 +04:00
self . filesize = @temp_file . size
end
end
end
2012-02-26 14:16:09 +04:00
2008-04-03 01:30:32 +04:00
def file
nil
end
2012-02-23 14:01:16 +04:00
def filename = ( arg )
write_attribute :filename , sanitize_filename ( arg . to_s )
filename
end
2009-04-10 20:37:42 +04:00
# Copies the temporary file to its final location
# and computes its MD5 hash
2011-08-30 08:11:48 +04:00
def files_to_final_location
2008-04-03 01:30:32 +04:00
if @temp_file && ( @temp_file . size > 0 )
2012-12-13 16:07:19 +04:00
self . disk_directory = target_directory
self . disk_filename = Attachment . disk_filename ( filename , disk_directory )
2013-10-05 15:37:57 +04:00
logger . info ( " Saving attachment ' #{ self . diskfile } ' ( #{ @temp_file . size } bytes) " ) if logger
2012-12-13 16:07:19 +04:00
path = File . dirname ( diskfile )
unless File . directory? ( path )
FileUtils . mkdir_p ( path )
end
2009-04-10 20:37:42 +04:00
md5 = Digest :: MD5 . new
2011-05-16 03:45:13 +04:00
File . open ( diskfile , " wb " ) do | f |
2012-05-05 13:19:23 +04:00
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 )
2009-04-10 20:37:42 +04:00
end
2008-04-03 01:30:32 +04:00
end
2009-04-10 20:37:42 +04:00
self . digest = md5 . hexdigest
2008-04-03 01:30:32 +04:00
end
2011-07-29 19:28:59 +04:00
@temp_file = nil
2008-04-03 01:30:32 +04:00
# 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
2012-01-20 21:56:28 +04:00
# Deletes the file from the file system if it's not referenced by other attachments
2011-08-30 09:08:21 +04:00
def delete_from_disk
2012-05-26 00:54:11 +04:00
if Attachment . where ( " disk_filename = ? AND id <> ? " , disk_filename , id ) . empty?
2012-01-20 21:56:28 +04:00
delete_from_disk!
end
2008-04-03 01:30:32 +04:00
end
# Returns file's location on disk
def diskfile
2012-12-13 16:07:19 +04:00
File . join ( self . class . storage_path , disk_directory . to_s , disk_filename . to_s )
2008-04-03 01:30:32 +04:00
end
2011-05-16 03:45:13 +04:00
2012-07-16 20:42:23 +04:00
def title
title = filename . to_s
if description . present?
title << " ( #{ description } ) "
end
title
end
2007-03-12 20:59:02 +03:00
def increment_download
increment! ( :downloads )
end
2007-05-26 19:42:37 +04:00
def project
2012-02-17 01:00:11 +04:00
container . try ( :project )
2007-05-26 19:42:37 +04:00
end
2011-05-16 03:45:13 +04:00
2008-12-09 19:54:46 +03:00
def visible? ( user = User . current )
2012-12-11 00:09:41 +04:00
if container_id
container && container . attachments_visible? ( user )
else
author == user
end
2008-12-09 19:54:46 +03:00
end
2011-05-16 03:45:13 +04:00
2008-12-09 19:54:46 +03:00
def deletable? ( user = User . current )
2012-12-11 00:09:41 +04:00
if container_id
container && container . attachments_deletable? ( user )
else
author == user
end
2008-12-09 19:54:46 +03:00
end
2011-05-16 03:45:13 +04:00
2007-08-15 19:36:15 +04:00
def image?
2012-07-07 17:48:07 +04:00
! ! ( 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.
2012-07-16 21:15:40 +04:00
def thumbnail ( options = { } )
2012-07-07 17:48:07 +04:00
if thumbnailable? && readable?
2012-07-16 21:15:40 +04:00
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
2012-07-07 17:48:07 +04:00
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
2007-08-15 19:36:15 +04:00
end
2011-05-16 03:45:13 +04:00
2008-06-09 22:40:59 +04:00
def is_text?
Redmine :: MimeType . is_type? ( 'text' , filename )
end
2011-05-16 03:45:13 +04:00
2008-06-08 22:26:39 +04:00
def is_diff?
self . filename =~ / \ .(patch|diff)$ /i
end
2011-05-16 03:45:13 +04:00
2009-04-25 13:31:36 +04:00
# Returns true if the file is readable
def readable?
File . readable? ( diskfile )
end
2010-03-02 22:26:03 +03:00
2012-02-17 01:00:11 +04:00
# 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
2012-05-26 00:51:43 +04:00
attachment = Attachment . where ( :id = > attachment_id , :digest = > attachment_digest ) . first
2012-02-17 01:00:11 +04:00
if attachment && attachment . container . nil?
attachment
end
end
end
2010-03-02 22:26:03 +03:00
# 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 )
2012-02-17 01:00:11 +04:00
result = obj . save_attachments ( attachments , User . current )
obj . attach_saved_attachments
result
2010-03-02 22:26:03 +03:00
end
2011-05-16 03:45:13 +04:00
2011-11-24 09:31:29 +04:00
def self . latest_attach ( attachments , filename )
2012-02-26 14:16:09 +04:00
attachments . sort_by ( & :created_on ) . reverse . detect {
2011-11-24 09:31:29 +04:00
| att | att . filename . downcase == filename . downcase
}
end
2012-02-17 01:00:11 +04:00
def self . prune ( age = 1 . day )
2012-05-26 00:51:43 +04:00
Attachment . where ( " created_on < ? AND (container_type IS NULL OR container_type = '') " , Time . now - age ) . destroy_all
2012-02-17 01:00:11 +04:00
end
2013-01-26 04:47:29 +04:00
# Moves an existing attachment to its target directory
2012-12-13 16:07:19 +04:00
def move_to_target_directory!
2013-11-09 13:46:55 +04:00
return unless ! new_record? & readable?
src = diskfile
self . disk_directory = target_directory
dest = diskfile
return if src == dest
if ! FileUtils . mkdir_p ( File . dirname ( dest ) )
logger . error " Could not create directory #{ File . dirname ( dest ) } " if logger
return
end
if ! FileUtils . mv ( src , dest )
logger . error " Could not move attachment from #{ src } to #{ dest } " if logger
return
2012-12-13 16:07:19 +04:00
end
2013-11-09 13:46:55 +04:00
update_column :disk_directory , disk_directory
2012-12-13 16:07:19 +04:00
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
2012-01-20 21:56:28 +04:00
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
2007-03-12 20:59:02 +03:00
def sanitize_filename ( value )
2008-04-03 01:30:32 +04:00
# get only the filename, not the whole path
2013-09-11 23:19:24 +04:00
just_filename = value . gsub ( / \ A.*( \\ | \/ ) /m , '' )
2007-03-12 20:59:02 +03:00
2011-11-25 00:17:56 +04:00
# Finally, replace invalid characters with underscore
2013-09-11 23:19:24 +04:00
@filename = just_filename . gsub ( / [ \/ \ ? \ % \ * \ : \ | \ " \ '<> \ n \ r]+ / , '_' )
2007-03-12 20:59:02 +03:00
end
2011-05-16 03:45:13 +04:00
2012-12-13 16:07:19 +04:00
# 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 )
2010-02-28 14:12:40 +03:00
timestamp = DateTime . now . strftime ( " %y%m%d%H%M%S " )
ascii = ''
2008-05-17 15:03:43 +04:00
if filename =~ %r{ ^[a-zA-Z0-9_ \ . \ -]*$ }
2010-02-28 14:12:40 +03:00
ascii = filename
2008-05-17 15:03:43 +04:00
else
2010-02-28 14:12:40 +03:00
ascii = Digest :: MD5 . hexdigest ( filename )
2008-05-17 15:03:43 +04:00
# keep the extension if any
2010-02-28 14:12:40 +03:00
ascii << $1 if filename =~ %r{ ( \ .[a-zA-Z0-9]+)$ }
2008-05-17 15:03:43 +04:00
end
2012-12-13 16:07:19 +04:00
while File . exist? ( File . join ( storage_path , directory . to_s , " #{ timestamp } _ #{ ascii } " ) )
2010-02-28 14:12:40 +03:00
timestamp . succ!
end
" #{ timestamp } _ #{ ascii } "
2008-05-17 15:03:43 +04:00
end
2006-06-28 22:11:03 +04:00
end