2014-10-23 11:30:34 +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
belongs_to :project , foreign_key : 'container_id' , conditions : " attachments.container_type = 'Project' "
belongs_to :course , foreign_key : 'container_id' , conditions : " attachments.container_type = 'Course' "
belongs_to :softapplication , foreign_key : 'container_id' , conditions : " attachments.container_type = 'Softapplication' "
belongs_to :author , :class_name = > " User " , :foreign_key = > " author_id "
belongs_to :attachmentstype , :foreign_key = > " attachtype " , :primary_key = > " id "
2015-03-04 15:32:00 +08:00
# 被ForgeActivity虚拟关联
has_many :forge_acts , :class_name = > 'ForgeActivity' , :as = > :forge_act , :dependent = > :destroy
2015-08-11 15:54:05 +08:00
# 课程动态
has_many :course_acts , :class_name = > 'CourseActivity' , :as = > :course_act , :dependent = > :destroy
2015-03-04 15:32:00 +08:00
# end
2014-10-23 11:30:34 +08:00
include UserScoreHelper
validates :filename , presence : true , length : { maximum : 254 }
validates :author , presence : true
validates :disk_filename , length : { maximum : 254 }
validates :description , length : { maximum : 254 }
validate :validate_max_file_size
acts_as_taggable
acts_as_event :title = > :filename ,
:url = > Proc . new { | o | { :controller = > 'attachments' , :action = > 'download' , :id = > o . id , :filename = > o . filename } }
#课程资源文件
acts_as_activity_provider :type = > 'course_files' ,
:is_public = > 'attachments.is_public' ,
:permission = > :view_files ,
:author_key = > :author_id ,
:find_options = > { :select = > " #{ Attachment . table_name } .* " ,
:joins = > " LEFT JOIN #{ Course . table_name } ON ( #{ Attachment . table_name } .container_type='Course' AND #{ Attachment . table_name } .container_id = #{ Course . table_name } .id ) " }
acts_as_activity_provider :type = > 'files' ,
:is_public = > 'attachments.is_public' ,
: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' ,
:is_public = > 'documents.is_public' ,
: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 " )
2015-09-25 16:53:23 +08:00
before_save :files_to_final_location , :act_as_course_activity
2015-08-11 15:54:05 +08:00
after_create :office_conver , :be_user_score , :act_as_forge_activity
2015-06-03 18:57:05 +08:00
after_update :office_conver , :be_user_score
2014-10-23 11:30:34 +08:00
after_destroy :delete_from_disk , :down_user_score
# add by nwb
# 获取所有可公开的资源文件列表
scope :public_attachments , lambda {
#joins(Project.table_name).where("container_type = 'Project' and ")
joins ( " LEFT JOIN #{ Project . table_name } ON #{ Attachment . table_name } .container_type='Project' AND #{ Project . table_name } .id = #{ Attachment . table_name } .container_id and #{ Project . table_name } .is_public=1 " +
" LEFT JOIN #{ Document . table_name } ON #{ Attachment . table_name } .container_type='Project' AND #{ Document . table_name } .project_id in " + self . public_project_id +
" LEFT JOIN #{ Issue . table_name } ON #{ Attachment . table_name } .container_type='Project' AND #{ Issue . table_name } .project_id in " + self . public_project_id +
" LEFT JOIN #{ Version . table_name } ON #{ Attachment . table_name } .container_type='Project' AND #{ Version . table_name } .project_id in " + self . public_project_id +
" LEFT JOIN #{ WikiPage . table_name } ON #{ Attachment . table_name } .container_type='WikiPage' AND #{ WikiPage . table_name } .parent_id in " + self . public_wiki_id +
" LEFT JOIN #{ Message . table_name } ON #{ Attachment . table_name } .container_type='Message' AND #{ Message . table_name } .parent_id in " + self . public_board_id +
" LEFT JOIN #{ Course . table_name } ON #{ Attachment . table_name } .container_type='Course' AND #{ Course . table_name } .is_public=1 " +
" LEFT JOIN #{ News . table_name } ON #{ Attachment . table_name } .container_type='News' AND ( #{ News . table_name } .project_id in " + self . public_project_id + " OR #{ News . table_name } .course_id in " + self . public_course_id + " ) " +
" LEFT JOIN #{ HomeworkAttach . table_name } ON #{ Attachment . table_name } .container_type='HomeworkAttach' AND #{ HomeworkAttach . table_name } .bid_id in " + self . public_bid_id )
}
# add by nwb
# 公开的项目id列表
def self . public_project_id
idlist = " ( "
projects = Project . all_public
count = projects . count
for i in 0 ... count
project = projects [ i ]
idlist += " ' " + project . id . to_s + " ' "
if i != count - 1
idlist += " , "
end
end
idlist += " ) "
idlist
end
# add by nwb
# 公开的课程id列表
def self . public_course_id
idlist = " ( "
courses = Course . all_public
count = courses . count
for i in 0 ... count
course = courses [ i ]
idlist += " ' " + course . id . to_s + " ' "
if i != count - 1
idlist = idlist + " , "
end
end
idlist += " ) "
idlist
end
# add by nwb
# 公开的wiki id列表
def self . public_wiki_id
idlist = " ( "
wikis = Wiki . where ( " project_id in " + public_project_id )
count = wikis . count
for i in 0 ... count
wiki = wikis [ i ]
idlist += " ' " + wiki . id . to_s + " ' "
if i != count - 1
idlist = idlist + " , "
end
end
idlist += " ) "
idlist
end
# add by nwb
# 公开的board id列表
def self . public_board_id
idlist = " ( "
boards = Board . where ( " project_id in " + public_project_id + " or course_id in " + public_course_id )
count = boards . count
for i in 0 ... count
board = boards [ i ]
idlist += " ' " + board . id . to_s + " ' "
if i != count - 1
idlist = idlist + " , "
end
end
idlist += " ) "
idlist
end
# add by nwb
# 公开的bid id列表
def self . public_bid_id
idlist = " ( "
bids = Bid . where ( " reward_type=3 " )
count = bids . count
for i in 0 ... count
bid = bids [ i ]
idlist += " ' " + bid . id . to_s + " ' "
if i != count - 1
idlist = idlist + " , "
end
end
idlist += " ) "
idlist
end
# 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 suffix_type
childArr = self . filename . split ( '.' )
suffix = '*'
if childArr . length > 1
suffix = childArr [ childArr . length - 1 ]
end
suffix
end
#获取用来显示的后缀名称
def show_suffix_type
suffix = 'other'
temp = self . suffix_type . downcase
if self . attachmentstype && self . attachmentstype . suffixArr . include? ( temp )
suffix = temp
end
suffix
end
# 文件密级的字符描述
def file_dense_str
if self . is_public == 1
dense = l ( :field_is_public )
else
dense = l ( :field_is_private )
end
dense
end
# 文件可设置的密级列表
def file_dense_list
denselist = [ l ( :field_is_public ) , l ( :field_is_private ) ]
end
def suffixArr
@@SuffixArr
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
# 允许上传文件大小为0的文件
#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
2015-06-03 11:14:33 +08:00
def office_conver
2015-06-04 10:24:05 +08:00
# 不在这里做后台转换,换为点击时做转换
# return unless %w(Project Course).include? (self.container_type)
# saved_path = File.join(Rails.root, "files", "convered_office")
# unless Dir.exist?(saved_path)
# Dir.mkdir(saved_path)
# end
# convered_file = File.join(saved_path, self.disk_filename + ".pdf")
# OfficeConverTask.new.conver(self.diskfile, convered_file)
2015-06-03 11:14:33 +08:00
end
2014-10-23 11:30:34 +08:00
# Copies the temporary file to its final location
# and computes its MD5 hash
def files_to_final_location
# # 允许上传文件大小为0的文件
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 course
container
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
container && ( container . is_a? ( Project ) ? container . attachments_deletable? ( user ) : true )
else
author == user
end
end
def image?
! ! ( self . filename =~ / \ .(bmp|gif|jpg|jpe|jpeg|png)$ /i )
end
def pack?
! ! ( self . filename =~ / \ .(zip|rar|tar|gz|exe|jar|7z|iso)$ /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 } \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 )
attachment = find_by_token_only ( token )
attachment if attachment . container . nil?
end
# Finds an attachment that matches the given token
def self . find_by_token_only ( 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
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
def self . attach_filesex ( obj , attachments , attachment_type )
result = obj . save_attachmentsex ( attachments , User . current , attachment_type )
obj . attach_saved_attachments
result
end
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
# update user score
def be_user_score
if self . container_id_changed?
type = self . container_type
types = %w|Document News Version Project Issue Message WikiPage|
if types . include? ( type )
#UserScore.project(:push_file, self.author,self, { attachment_id: self.id })
end
end
update_attachment ( self . author , 1 )
if self . container_type == 'Project'
update_attachment ( self . author , 2 , self . container )
end
end
#删除附件时重新统计用户的附件数量得分
def down_user_score
update_attachment ( self . author , 1 )
if self . container_type == 'Project'
update_attachment ( self . author , 2 , self . container )
end
end
2015-03-04 15:32:00 +08:00
# Time 2015-03-02 17:42:48
# Author lizanle
# Description 上传该项目的文档资料也要保存一份在公共表中
def act_as_forge_activity
if self . container_type == 'Project'
self . forge_acts << ForgeActivity . new ( :user_id = > self . author_id ,
:project_id = > self . container_id )
end
end
2015-08-11 15:54:05 +08:00
#课程动态公共表记录
def act_as_course_activity
if self . container_type == " Course " && self . course_acts . empty?
self . course_acts << CourseActivity . new ( :user_id = > self . author_id , :course_id = > self . container_id )
end
end
2014-10-23 11:30:34 +08:00
end