class AttachmentsController < ApplicationController
before_filter :find_project, :except => :upload
- before_filter :file_readable, :read_authorize, :only => [:show, :download]
+ before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
before_filter :delete_authorize, :only => :destroy
before_filter :authorize_global, :only => :upload
end
+ def thumbnail
+ if @attachment.thumbnailable? && Setting.thumbnails_enabled? && thumbnail = @attachment.thumbnail
+ send_file thumbnail,
+ :filename => filename_for_content_disposition(@attachment.filename),
+ :type => detect_content_type(@attachment),
+ :disposition => 'inline'
+ else
+ # No thumbnail for the attachment or thumbnail could not be created
+ render :nothing => true, :status => 404
+ end
+ end
+
def upload
# Make sure that API users get used to set this content type
# as it won't trigger Rails' automatic parsing of the request body for parameters
end
end
+ def thumbnail_tag(attachment)
+ link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
+ {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
+ :title => attachment.filename
+ end
+
def toggle_link(name, id, options={})
onclick = "Element.toggle('#{id}'); "
onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
# Displays view/delete links to the attachments of the given object
# Options:
# :author -- author names are not displayed if set to false
+ # :thumbails -- display thumbnails if enabled in settings
def link_to_attachments(container, options = {})
- options.assert_valid_keys(:author)
+ options.assert_valid_keys(:author, :thumbnails)
if container.attachments.any?
options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
- render :partial => 'attachments/links', :locals => {:attachments => container.attachments, :options => options}
+ render :partial => 'attachments/links',
+ :locals => {:attachments => container.attachments, :options => options, :thumbnails => (options[:thumbnails] && Setting.thumbnails_enabled?)}
end
end
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
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
+ if thumbnailable? && readable?
+ size = Setting.thumbnails_size.to_i
+ 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?
<% end %>
</p>
<% end %>
+<% if defined?(thumbnails) && thumbnails %>
+ <% images = attachments.select(&:thumbnailable?) %>
+ <% if images.any? %>
+ <div class="thumbnails">
+ <% images.each do |attachment| %>
+ <div><%= thumbnail_tag(attachment) %></div>
+ <% end %>
+ </div>
+ <% end %>
+<% end %>
</div>
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
</div>
<% end %>
-<%= link_to_attachments @issue %>
+<%= link_to_attachments @issue, :thumbnails => true %>
<% end -%>
<%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
<p><%= setting_check_box :gravatar_enabled %></p>
<p><%= setting_select :gravatar_default, [["Wavatars", 'wavatar'], ["Identicons", 'identicon'], ["Monster ids", 'monsterid'], ["Retro", 'retro'], ["Mystery man", 'mm']], :blank => :label_none %></p>
+
+<p><%= setting_check_box :thumbnails_enabled %></p>
+
+<p><%= setting_text_field :thumbnails_size, :size => 6 %></p>
</div>
<%= submit_tag l(:button_save) %>
# same secret token on each machine.
#secret_token: 'change it to a long random string'
+ # Absolute path (e.g. /usr/bin/convert, c:/im/convert.exe) to
+ # the ImageMagick's `convert` binary. Used to generate attachment thumbnails.
+ #imagemagick_convert_command:
+
# specific configuration options for production environment
# that overrides the default ones
production:
setting_unsubscribe: Allow users to delete their own account
setting_session_lifetime: Session maximum lifetime
setting_session_timeout: Session inactivity timeout
+ setting_thumbnails_enabled: Display attachment thumbnails
+ setting_thumbnails_size: Thumbnails size (in pixels)
permission_add_project: Create project
permission_add_subprojects: Create subprojects
setting_unsubscribe: Permettre aux utilisateurs de supprimer leur propre compte
setting_session_lifetime: Durée de vie maximale des sessions
setting_session_timeout: Durée maximale d'inactivité
+ setting_thumbnails_enabled: Afficher les vignettes des images
+ setting_thumbnails_size: Taille des vignettes (en pixels)
permission_add_project: Créer un projet
permission_add_subprojects: Créer des sous-projets
match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get
match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get
match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get
+ match 'attachments/thumbnail/:id', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get
resources :attachments, :only => [:show, :destroy]
resources :groups do
default: 'only_my_events'
emails_header:
default: ''
+thumbnails_enabled:
+ default: 0
+thumbnails_size:
+ format: int
+ default: 100
--- /dev/null
+# Redmine - project management software
+# 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
+# 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 'fileutils'
+
+module Redmine
+ module Thumbnail
+ extend Redmine::Utils::Shell
+
+ # Generates a thumbnail for the source image to target
+ def self.generate(source, target, size)
+ unless File.exists?(target)
+ directory = File.dirname(target)
+ unless File.exists?(directory)
+ FileUtils.mkdir_p directory
+ end
+ bin = Redmine::Configuration['imagemagick_convert_command'] || 'convert'
+ size_option = "#{size}x#{size}>"
+ cmd = "#{shell_quote bin} #{shell_quote source} -thumbnail #{shell_quote size_option} #{shell_quote target}"
+ unless system(cmd)
+ logger.error("Creating thumbnail failed (#{$?}):\nCommand: #{cmd}")
+ return nil
+ end
+ end
+ target
+ end
+
+ def self.logger
+ Rails.logger
+ end
+ end
+end
SecureRandom.hex(n)
end
end
+
+ module Shell
+ def shell_quote(str)
+ if Redmine::Platform.mswin?
+ '"' + str.gsub(/"/, '\\"') + '"'
+ else
+ "'" + str.gsub(/'/, "'\"'\"'") + "'"
+ end
+ end
+ end
end
end
div.attachments img { vertical-align: middle; }
div.attachments span.author { font-size: 0.9em; color: #888; }
+div.thumbnails {margin-top:0.6em;}
+div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
+div.thumbnails img {margin: 3px;}
+
p.other-formats { text-align: right; font-size:0.9em; color: #666; }
.other-formats span + span:before { content: "| "; }
set_tmp_attachments_directory
end
- def test_anonymous_on_private_private
+ def test_download_should_be_denied_without_permission
get :download, :id => 7
assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7'
set_tmp_attachments_directory
end
+ if convert_installed?
+ def test_thumbnail
+ Attachment.clear_thumbnails
+ @request.session[:user_id] = 2
+ with_settings :thumbnails_enabled => '1' do
+ get :thumbnail, :id => 16
+ assert_response :success
+ assert_equal 'image/png', response.content_type
+ end
+ end
+
+ def test_thumbnail_should_return_404_for_non_image_attachment
+ @request.session[:user_id] = 2
+ with_settings :thumbnails_enabled => '1' do
+ get :thumbnail, :id => 15
+ assert_response 404
+ end
+ end
+
+ def test_thumbnail_should_return_404_if_thumbnails_not_enabled
+ @request.session[:user_id] = 2
+ with_settings :thumbnails_enabled => '0' do
+ get :thumbnail, :id => 16
+ assert_response 404
+ end
+ end
+
+ def test_thumbnail_should_return_404_if_thumbnail_generation_failed
+ Attachment.any_instance.stubs(:thumbnail).returns(nil)
+ @request.session[:user_id] = 2
+ with_settings :thumbnails_enabled => '1' do
+ get :thumbnail, :id => 16
+ assert_response 404
+ end
+ end
+
+ def test_thumbnail_should_be_denied_without_permission
+ with_settings :thumbnails_enabled => '1' do
+ get :thumbnail, :id => 16
+ assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fthumbnail%2F16'
+ end
+ end
+ else
+ puts '(ImageMagick convert not available)'
+ end
+
def test_destroy_issue_attachment
set_tmp_attachments_directory
issue = Issue.find(3)
end
end
end
-
+
+ def test_show_with_thumbnails_enabled_should_display_thumbnails
+ @request.session[:user_id] = 2
+
+ with_settings :thumbnails_enabled => '1' do
+ get :show, :id => 14
+ assert_response :success
+ end
+
+ assert_select 'div.thumbnails' do
+ assert_select 'a[href=/attachments/16/testfile.png]' do
+ assert_select 'img[src=/attachments/thumbnail/16]'
+ end
+ end
+ end
+
+ def test_show_with_thumbnails_disabled_should_not_display_thumbnails
+ @request.session[:user_id] = 2
+
+ with_settings :thumbnails_enabled => '0' do
+ get :show, :id => 14
+ assert_response :success
+ end
+
+ assert_select 'div.thumbnails', 0
+ end
+
def test_show_with_multi_custom_field
field = CustomField.find(1)
field.update_attribute :multiple, true
{ :controller => 'attachments', :action => 'download', :id => '1',
:filename => 'filename.ext' }
)
+ assert_routing(
+ { :method => 'get', :path => "/attachments/thumbnail/1" },
+ { :controller => 'attachments', :action => 'thumbnail', :id => '1' }
+ )
assert_routing(
{ :method => 'delete', :path => "/attachments/1" },
{ :controller => 'attachments', :action => 'destroy', :id => '1' }
return nil
end
+ def self.convert_installed?
+ bin = Redmine::Configuration['imagemagick_convert_command'] || 'convert'
+ system("#{bin} -version")
+ rescue
+ false
+ end
+
# Returns the path to the test +vendor+ repository
def self.repository_path(vendor)
Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
set_tmp_attachments_directory
end
+
+ def test_thumbnailable_should_be_true_for_images
+ assert_equal true, Attachment.new(:filename => 'test.jpg').thumbnailable?
+ end
+
+ def test_thumbnailable_should_be_true_for_non_images
+ assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable?
+ end
+
+ if convert_installed?
+ def test_thumbnail_should_generate_the_thumbnail
+ set_fixtures_attachments_directory
+ attachment = Attachment.find(16)
+ Attachment.clear_thumbnails
+
+ assert_difference "Dir.glob(File.join(Attachment.thumbnails_storage_path, '*.thumb')).size" do
+ thumbnail = attachment.thumbnail
+ assert_equal "16_8e0294de2441577c529f170b6fb8f638_100.thumb", File.basename(thumbnail)
+ assert File.exists?(thumbnail)
+ end
+ end
+ else
+ puts '(ImageMagick convert not available)'
+ end
end