##// END OF EJS Templates
Makes the folder for attachments storage configurable in config/configuration.yml (#1236)....
Jean-Philippe Lang -
r4635:7dd464c577bd
parent child
Show More
@@ -1,191 +1,191
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/md5"
18 require "digest/md5"
19
19
20 class Attachment < ActiveRecord::Base
20 class Attachment < ActiveRecord::Base
21 belongs_to :container, :polymorphic => true
21 belongs_to :container, :polymorphic => true
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23
23
24 validates_presence_of :container, :filename, :author
24 validates_presence_of :container, :filename, :author
25 validates_length_of :filename, :maximum => 255
25 validates_length_of :filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
27
27
28 acts_as_event :title => :filename,
28 acts_as_event :title => :filename,
29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30
30
31 acts_as_activity_provider :type => 'files',
31 acts_as_activity_provider :type => 'files',
32 :permission => :view_files,
32 :permission => :view_files,
33 :author_key => :author_id,
33 :author_key => :author_id,
34 :find_options => {:select => "#{Attachment.table_name}.*",
34 :find_options => {:select => "#{Attachment.table_name}.*",
35 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
35 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
36 "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 )"}
36 "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 )"}
37
37
38 acts_as_activity_provider :type => 'documents',
38 acts_as_activity_provider :type => 'documents',
39 :permission => :view_documents,
39 :permission => :view_documents,
40 :author_key => :author_id,
40 :author_key => :author_id,
41 :find_options => {:select => "#{Attachment.table_name}.*",
41 :find_options => {:select => "#{Attachment.table_name}.*",
42 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
42 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
43 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
43 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
44
44
45 cattr_accessor :storage_path
45 cattr_accessor :storage_path
46 @@storage_path = "#{RAILS_ROOT}/files"
46 @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{RAILS_ROOT}/files"
47
47
48 def validate
48 def validate
49 if self.filesize > Setting.attachment_max_size.to_i.kilobytes
49 if self.filesize > Setting.attachment_max_size.to_i.kilobytes
50 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
50 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
51 end
51 end
52 end
52 end
53
53
54 def file=(incoming_file)
54 def file=(incoming_file)
55 unless incoming_file.nil?
55 unless incoming_file.nil?
56 @temp_file = incoming_file
56 @temp_file = incoming_file
57 if @temp_file.size > 0
57 if @temp_file.size > 0
58 self.filename = sanitize_filename(@temp_file.original_filename)
58 self.filename = sanitize_filename(@temp_file.original_filename)
59 self.disk_filename = Attachment.disk_filename(filename)
59 self.disk_filename = Attachment.disk_filename(filename)
60 self.content_type = @temp_file.content_type.to_s.chomp
60 self.content_type = @temp_file.content_type.to_s.chomp
61 if content_type.blank?
61 if content_type.blank?
62 self.content_type = Redmine::MimeType.of(filename)
62 self.content_type = Redmine::MimeType.of(filename)
63 end
63 end
64 self.filesize = @temp_file.size
64 self.filesize = @temp_file.size
65 end
65 end
66 end
66 end
67 end
67 end
68
68
69 def file
69 def file
70 nil
70 nil
71 end
71 end
72
72
73 # Copies the temporary file to its final location
73 # Copies the temporary file to its final location
74 # and computes its MD5 hash
74 # and computes its MD5 hash
75 def before_save
75 def before_save
76 if @temp_file && (@temp_file.size > 0)
76 if @temp_file && (@temp_file.size > 0)
77 logger.debug("saving '#{self.diskfile}'")
77 logger.debug("saving '#{self.diskfile}'")
78 md5 = Digest::MD5.new
78 md5 = Digest::MD5.new
79 File.open(diskfile, "wb") do |f|
79 File.open(diskfile, "wb") do |f|
80 buffer = ""
80 buffer = ""
81 while (buffer = @temp_file.read(8192))
81 while (buffer = @temp_file.read(8192))
82 f.write(buffer)
82 f.write(buffer)
83 md5.update(buffer)
83 md5.update(buffer)
84 end
84 end
85 end
85 end
86 self.digest = md5.hexdigest
86 self.digest = md5.hexdigest
87 end
87 end
88 # Don't save the content type if it's longer than the authorized length
88 # Don't save the content type if it's longer than the authorized length
89 if self.content_type && self.content_type.length > 255
89 if self.content_type && self.content_type.length > 255
90 self.content_type = nil
90 self.content_type = nil
91 end
91 end
92 end
92 end
93
93
94 # Deletes file on the disk
94 # Deletes file on the disk
95 def after_destroy
95 def after_destroy
96 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
96 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
97 end
97 end
98
98
99 # Returns file's location on disk
99 # Returns file's location on disk
100 def diskfile
100 def diskfile
101 "#{@@storage_path}/#{self.disk_filename}"
101 "#{@@storage_path}/#{self.disk_filename}"
102 end
102 end
103
103
104 def increment_download
104 def increment_download
105 increment!(:downloads)
105 increment!(:downloads)
106 end
106 end
107
107
108 def project
108 def project
109 container.project
109 container.project
110 end
110 end
111
111
112 def visible?(user=User.current)
112 def visible?(user=User.current)
113 container.attachments_visible?(user)
113 container.attachments_visible?(user)
114 end
114 end
115
115
116 def deletable?(user=User.current)
116 def deletable?(user=User.current)
117 container.attachments_deletable?(user)
117 container.attachments_deletable?(user)
118 end
118 end
119
119
120 def image?
120 def image?
121 self.filename =~ /\.(jpe?g|gif|png)$/i
121 self.filename =~ /\.(jpe?g|gif|png)$/i
122 end
122 end
123
123
124 def is_text?
124 def is_text?
125 Redmine::MimeType.is_type?('text', filename)
125 Redmine::MimeType.is_type?('text', filename)
126 end
126 end
127
127
128 def is_diff?
128 def is_diff?
129 self.filename =~ /\.(patch|diff)$/i
129 self.filename =~ /\.(patch|diff)$/i
130 end
130 end
131
131
132 # Returns true if the file is readable
132 # Returns true if the file is readable
133 def readable?
133 def readable?
134 File.readable?(diskfile)
134 File.readable?(diskfile)
135 end
135 end
136
136
137 # Bulk attaches a set of files to an object
137 # Bulk attaches a set of files to an object
138 #
138 #
139 # Returns a Hash of the results:
139 # Returns a Hash of the results:
140 # :files => array of the attached files
140 # :files => array of the attached files
141 # :unsaved => array of the files that could not be attached
141 # :unsaved => array of the files that could not be attached
142 def self.attach_files(obj, attachments)
142 def self.attach_files(obj, attachments)
143 attached = []
143 attached = []
144 if attachments && attachments.is_a?(Hash)
144 if attachments && attachments.is_a?(Hash)
145 attachments.each_value do |attachment|
145 attachments.each_value do |attachment|
146 file = attachment['file']
146 file = attachment['file']
147 next unless file && file.size > 0
147 next unless file && file.size > 0
148 a = Attachment.create(:container => obj,
148 a = Attachment.create(:container => obj,
149 :file => file,
149 :file => file,
150 :description => attachment['description'].to_s.strip,
150 :description => attachment['description'].to_s.strip,
151 :author => User.current)
151 :author => User.current)
152
152
153 if a.new_record?
153 if a.new_record?
154 obj.unsaved_attachments ||= []
154 obj.unsaved_attachments ||= []
155 obj.unsaved_attachments << a
155 obj.unsaved_attachments << a
156 else
156 else
157 attached << a
157 attached << a
158 end
158 end
159 end
159 end
160 end
160 end
161 {:files => attached, :unsaved => obj.unsaved_attachments}
161 {:files => attached, :unsaved => obj.unsaved_attachments}
162 end
162 end
163
163
164 private
164 private
165 def sanitize_filename(value)
165 def sanitize_filename(value)
166 # get only the filename, not the whole path
166 # get only the filename, not the whole path
167 just_filename = value.gsub(/^.*(\\|\/)/, '')
167 just_filename = value.gsub(/^.*(\\|\/)/, '')
168 # NOTE: File.basename doesn't work right with Windows paths on Unix
168 # NOTE: File.basename doesn't work right with Windows paths on Unix
169 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
169 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
170
170
171 # Finally, replace all non alphanumeric, hyphens or periods with underscore
171 # Finally, replace all non alphanumeric, hyphens or periods with underscore
172 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
172 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
173 end
173 end
174
174
175 # Returns an ASCII or hashed filename
175 # Returns an ASCII or hashed filename
176 def self.disk_filename(filename)
176 def self.disk_filename(filename)
177 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
177 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
178 ascii = ''
178 ascii = ''
179 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
179 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
180 ascii = filename
180 ascii = filename
181 else
181 else
182 ascii = Digest::MD5.hexdigest(filename)
182 ascii = Digest::MD5.hexdigest(filename)
183 # keep the extension if any
183 # keep the extension if any
184 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
184 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
185 end
185 end
186 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
186 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
187 timestamp.succ!
187 timestamp.succ!
188 end
188 end
189 "#{timestamp}_#{ascii}"
189 "#{timestamp}_#{ascii}"
190 end
190 end
191 end
191 end
@@ -1,98 +1,108
1 # = Redmine configuration file
1 # = Redmine configuration file
2 #
2 #
3 # Each environment has it's own configuration options. If you are only
3 # Each environment has it's own configuration options. If you are only
4 # running in production, only the production block needs to be configured.
4 # running in production, only the production block needs to be configured.
5 # Environment specific configuration options override the default ones.
5 # Environment specific configuration options override the default ones.
6 #
6 #
7 # Note that this file needs to be a valid YAML file.
7 # Note that this file needs to be a valid YAML file.
8 #
8 #
9 # == Outgoing email settings (email_delivery setting)
9 # == Outgoing email settings (email_delivery setting)
10 #
10 #
11 # === Common configurations
11 # === Common configurations
12 #
12 #
13 # ==== Sendmail command
13 # ==== Sendmail command
14 #
14 #
15 # production:
15 # production:
16 # email_delivery:
16 # email_delivery:
17 # delivery_method: :sendmail
17 # delivery_method: :sendmail
18 #
18 #
19 # ==== Simple SMTP server at localhost
19 # ==== Simple SMTP server at localhost
20 #
20 #
21 # production:
21 # production:
22 # email_delivery:
22 # email_delivery:
23 # delivery_method: :smtp
23 # delivery_method: :smtp
24 # smtp_settings:
24 # smtp_settings:
25 # address: "localhost"
25 # address: "localhost"
26 # port: 25
26 # port: 25
27 #
27 #
28 # ==== SMTP server at example.com using LOGIN authentication and checking HELO for foo.com
28 # ==== SMTP server at example.com using LOGIN authentication and checking HELO for foo.com
29 #
29 #
30 # production:
30 # production:
31 # email_delivery:
31 # email_delivery:
32 # delivery_method: :smtp
32 # delivery_method: :smtp
33 # smtp_settings:
33 # smtp_settings:
34 # address: "example.com"
34 # address: "example.com"
35 # port: 25
35 # port: 25
36 # authentication: :login
36 # authentication: :login
37 # domain: 'foo.com'
37 # domain: 'foo.com'
38 # user_name: 'myaccount'
38 # user_name: 'myaccount'
39 # password: 'password'
39 # password: 'password'
40 #
40 #
41 # ==== SMTP server at example.com using PLAIN authentication
41 # ==== SMTP server at example.com using PLAIN authentication
42 #
42 #
43 # production:
43 # production:
44 # email_delivery:
44 # email_delivery:
45 # delivery_method: :smtp
45 # delivery_method: :smtp
46 # smtp_settings:
46 # smtp_settings:
47 # address: "example.com"
47 # address: "example.com"
48 # port: 25
48 # port: 25
49 # authentication: :plain
49 # authentication: :plain
50 # domain: 'example.com'
50 # domain: 'example.com'
51 # user_name: 'myaccount'
51 # user_name: 'myaccount'
52 # password: 'password'
52 # password: 'password'
53 #
53 #
54 # ==== SMTP server at using TLS (GMail)
54 # ==== SMTP server at using TLS (GMail)
55 #
55 #
56 # This requires some additional configuration. See the article at:
56 # This requires some additional configuration. See the article at:
57 # http://redmineblog.com/articles/setup-redmine-to-send-email-using-gmail/
57 # http://redmineblog.com/articles/setup-redmine-to-send-email-using-gmail/
58 #
58 #
59 # production:
59 # production:
60 # email_delivery:
60 # email_delivery:
61 # delivery_method: :smtp
61 # delivery_method: :smtp
62 # smtp_settings:
62 # smtp_settings:
63 # tls: true
63 # tls: true
64 # address: "smtp.gmail.com"
64 # address: "smtp.gmail.com"
65 # port: 587
65 # port: 587
66 # domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps
66 # domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps
67 # authentication: :plain
67 # authentication: :plain
68 # user_name: "your_email@gmail.com"
68 # user_name: "your_email@gmail.com"
69 # password: "your_password"
69 # password: "your_password"
70 #
70 #
71 #
71 #
72 # === More configuration options
72 # === More configuration options
73 #
73 #
74 # See the "Configuration options" at the following website for a list of the
74 # See the "Configuration options" at the following website for a list of the
75 # full options allowed:
75 # full options allowed:
76 #
76 #
77 # http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMailer
77 # http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMailer
78
78
79
79
80 # default configuration options for all environments
80 # default configuration options for all environments
81 default:
81 default:
82 # Outgoing emails configuration (see examples above)
82 email_delivery:
83 email_delivery:
83 delivery_method: :smtp
84 delivery_method: :smtp
84 smtp_settings:
85 smtp_settings:
85 address: smtp.example.net
86 address: smtp.example.net
86 port: 25
87 port: 25
87 domain: example.net
88 domain: example.net
88 authentication: :login
89 authentication: :login
89 user_name: "redmine@example.net"
90 user_name: "redmine@example.net"
90 password: "redmine"
91 password: "redmine"
91
92
93 # Absolute path to the directory where attachments are stored.
94 # The default is the 'files' directory in your Redmine instance.
95 # Your Redmine instance needs to have write permission on this
96 # directory.
97 # Examples:
98 # attachments_storage_path: /var/redmine/files
99 # attachments_storage_path: D:/redmine/files
100 attachments_storage_path:
101
92 # specific configuration options for production environment
102 # specific configuration options for production environment
93 # that overrides the default ones
103 # that overrides the default ones
94 production:
104 production:
95
105
96 # specific configuration options for development environment
106 # specific configuration options for development environment
97 # that overrides the default ones
107 # that overrides the default ones
98 development:
108 development:
General Comments 0
You need to be logged in to leave comments. Login now