Feature #7056 » compress_the_all_attachments_in_issue_v2.patch
Gemfile | ||
---|---|---|
13 | 13 |
gem "mimemagic" |
14 | 14 |
gem "mail", "~> 2.7.1" |
15 | 15 |
gem "csv", "~> 3.0.1" if RUBY_VERSION >= "2.3" && RUBY_VERSION < "2.6" |
16 |
gem "rubyzip", "~> 1.2.1" |
|
16 | 17 | |
17 | 18 |
gem "nokogiri", "~> 1.9.0" |
18 | 19 |
gem "i18n", "~> 0.7.0" |
app/controllers/attachments_controller.rb | ||
---|---|---|
17 | 17 | |
18 | 18 |
class AttachmentsController < ApplicationController |
19 | 19 |
before_action :find_attachment, :only => [:show, :download, :thumbnail, :update, :destroy] |
20 |
before_action :find_container, :only => [:edit_all, :update_all, :download_all] |
|
21 |
before_action :find_downloadable_attachments, :only => :download_all |
|
20 | 22 |
before_action :find_editable_attachments, :only => [:edit_all, :update_all] |
21 | 23 |
before_action :file_readable, :read_authorize, :only => [:show, :download, :thumbnail] |
22 | 24 |
before_action :update_authorize, :only => :update |
... | ... | |
129 | 131 |
render :action => 'edit_all' |
130 | 132 |
end |
131 | 133 | |
134 |
def download_all |
|
135 |
Tempfile.create('attachments_zip', Rails.root.join('tmp')) do |tempfile| |
|
136 |
zip_file = Attachment.attachments_to_zip(tempfile, @attachments) |
|
137 |
if zip_file.nil? |
|
138 |
render_404 |
|
139 |
return |
|
140 |
end |
|
141 |
send_data(File.read(zip_file.path), :type => 'application/zip', |
|
142 |
:filename => "#{@container.class.to_s.downcase}-#{@container.id}-attachments.zip") |
|
143 |
end |
|
144 |
end |
|
145 | ||
132 | 146 |
def update |
133 | 147 |
@attachment.safe_attributes = params[:attachment] |
134 | 148 |
saved = @attachment.save |
... | ... | |
192 | 206 |
end |
193 | 207 | |
194 | 208 |
def find_editable_attachments |
209 |
@attachments = @container.attachments.select(&:editable?) |
|
210 |
render_404 if @attachments.empty? |
|
211 |
end |
|
212 | ||
213 |
def find_container |
|
195 | 214 |
klass = params[:object_type].to_s.singularize.classify.constantize rescue nil |
196 | 215 |
unless klass && klass.reflect_on_association(:attachments) |
197 | 216 |
render_404 |
... | ... | |
203 | 222 |
render_403 |
204 | 223 |
return |
205 | 224 |
end |
206 |
@attachments = @container.attachments.select(&:editable?) |
|
207 | 225 |
if @container.respond_to?(:project) |
208 | 226 |
@project = @container.project |
209 | 227 |
end |
210 |
render_404 if @attachments.empty? |
|
211 | 228 |
rescue ActiveRecord::RecordNotFound |
212 | 229 |
render_404 |
213 | 230 |
end |
214 | 231 | |
232 |
def find_downloadable_attachments |
|
233 |
@attachments = @container.attachments.select{|a| File.readable?(a.diskfile) } |
|
234 |
bulk_download_max_size = Redmine::Configuration['bulk_download_max_size'].to_i.kilobytes |
|
235 |
if @attachments.sum(&:filesize) > bulk_download_max_size |
|
236 |
flash[:error] = l(:error_bulk_download_size_too_big, |
|
237 |
:max_size => bulk_download_max_size) |
|
238 |
redirect_to back_url |
|
239 |
return |
|
240 |
end |
|
241 |
end |
|
242 | ||
215 | 243 |
# Checks that the file exists and is readable |
216 | 244 |
def file_readable |
217 | 245 |
if @attachment.readable? |
app/helpers/attachments_helper.rb | ||
---|---|---|
27 | 27 |
object_attachments_path container.class.name.underscore.pluralize, container.id |
28 | 28 |
end |
29 | 29 | |
30 |
def container_attachments_download_path(container) |
|
31 |
object_attachments_download_path container.class.name.underscore.pluralize, container.id |
|
32 |
end |
|
33 | ||
30 | 34 |
# Displays view/delete links to the attachments of the given object |
31 | 35 |
# Options: |
32 | 36 |
# :author -- author names are not displayed if set to false |
app/models/attachment.rb | ||
---|---|---|
17 | 17 | |
18 | 18 |
require "digest" |
19 | 19 |
require "fileutils" |
20 |
require "zip" |
|
20 | 21 | |
21 | 22 |
class Attachment < ActiveRecord::Base |
22 | 23 |
include Redmine::SafeAttributes |
... | ... | |
333 | 334 |
Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all |
334 | 335 |
end |
335 | 336 | |
337 |
def self.attachments_to_zip(tmpfile, attachments) |
|
338 |
attachments = attachments.select{|attachment| File.readable?(attachment.diskfile) } |
|
339 |
return nil if attachments.blank? |
|
340 |
Zip.unicode_names = true |
|
341 |
existing_file_names = [] |
|
342 |
Zip::File.open(tmpfile.path, Zip::File::CREATE) do |zip| |
|
343 |
attachments.each do |attachment| |
|
344 |
filename = attachment.filename |
|
345 |
existing_file_names << filename |
|
346 |
# If a file with the same name already exists, change the file name. |
|
347 |
if existing_file_names.count(filename) > 1 |
|
348 |
filename = "#{File.basename(filename, ".*")}(#{existing_file_names.count(filename)})#{File.extname(filename)}" |
|
349 |
end |
|
350 |
zip.add(filename, attachment.diskfile) |
|
351 |
end |
|
352 |
end |
|
353 |
tmpfile |
|
354 |
end |
|
355 | ||
336 | 356 |
# Moves an existing attachment to its target directory |
337 | 357 |
def move_to_target_directory! |
338 | 358 |
return unless !new_record? & readable? |
app/views/attachments/_links.html.erb | ||
---|---|---|
5 | 5 |
:title => l(:label_edit_attachments), |
6 | 6 |
:class => 'icon-only icon-edit' |
7 | 7 |
) if options[:editable] %> |
8 |
<%= link_to(l(:label_download_attachments), |
|
9 |
container_attachments_download_path(container), |
|
10 |
:title => l(:label_download_attachments), |
|
11 |
:class => 'icon-only icon-download' |
|
12 |
) %> |
|
8 | 13 |
</div> |
9 | 14 |
<table> |
10 | 15 |
<% for attachment in attachments %> |
config/configuration.yml.example | ||
---|---|---|
209 | 209 |
# allowed values: :memory, :file, :memcache |
210 | 210 |
#openid_authentication_store: :memory |
211 | 211 | |
212 |
# Configre maximum size of zip file(KB) |
|
213 |
# bulk_download_max_size: 51200 |
|
214 | ||
212 | 215 |
# specific configuration options for production environment |
213 | 216 |
# that overrides the default ones |
214 | 217 |
production: |
config/locales/en.yml | ||
---|---|---|
207 | 207 |
error_unable_delete_issue_status: 'Unable to delete issue status' |
208 | 208 |
error_unable_to_connect: "Unable to connect (%{value})" |
209 | 209 |
error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})" |
210 |
error_bulk_download_size_too_big: "These attachments cannot be bulk download because it exceeds the maximum allowed bulk download size (%{max_size})" |
|
210 | 211 |
error_session_expired: "Your session has expired. Please login again." |
211 | 212 |
error_token_expired: "This password recovery link has expired, please try again." |
212 | 213 |
warning_attachments_not_saved: "%{count} file(s) could not be saved." |
... | ... | |
994 | 995 |
label_users_visibility_all: All active users |
995 | 996 |
label_users_visibility_members_of_visible_projects: Members of visible projects |
996 | 997 |
label_edit_attachments: Edit attached files |
998 |
label_download_attachments: Download attached files |
|
997 | 999 |
label_link_copied_issue: Link copied issue |
998 | 1000 |
label_ask: Ask |
999 | 1001 |
label_search_attachments_yes: Search attachment filenames and descriptions |
config/locales/ja.yml | ||
---|---|---|
1011 | 1011 |
button_export: エクスポート |
1012 | 1012 |
label_export_options: "%{export_format} エクスポート設定" |
1013 | 1013 |
error_attachment_too_big: このファイルはアップロードできません。添付ファイルサイズの上限(%{max_size})を超えています。 |
1014 |
error_bulk_download_size_too_big: これらの添付ファイルをダウンロードできません。一括ダウンロードサイズの上限(%{max_size})を超えています。 |
|
1014 | 1015 |
notice_failed_to_save_time_entries: "全%{total}件中%{count}件の作業時間が保存できませんでした: %{ids}。" |
1015 | 1016 |
label_x_issues: |
1016 | 1017 |
zero: 0 チケット |
config/routes.rb | ||
---|---|---|
275 | 275 |
resources :attachments, :only => [:show, :update, :destroy] |
276 | 276 |
get 'attachments/:object_type/:object_id/edit', :to => 'attachments#edit_all', :as => :object_attachments_edit |
277 | 277 |
patch 'attachments/:object_type/:object_id', :to => 'attachments#update_all', :as => :object_attachments |
278 |
get 'attachments/:object_type/:object_id/download', :to => 'attachments#download_all', :as => :object_attachments_download |
|
278 | 279 | |
279 | 280 |
resources :groups do |
280 | 281 |
resources :memberships, :controller => 'principal_memberships' |
lib/redmine/configuration.rb | ||
---|---|---|
21 | 21 |
# Configuration default values |
22 | 22 |
@defaults = { |
23 | 23 |
'email_delivery' => nil, |
24 |
'max_concurrent_ajax_uploads' => 2 |
|
24 |
'max_concurrent_ajax_uploads' => 2, |
|
25 |
'bulk_download_max_size' => 51200 |
|
25 | 26 |
} |
26 | 27 | |
27 | 28 |
@config = nil |
test/functional/attachments_controller_test.rb | ||
---|---|---|
543 | 543 |
assert_equal 'This is a Ruby source file', attachment.description |
544 | 544 |
end |
545 | 545 | |
546 |
def test_download_all_with_valid_container |
|
547 |
@request.session[:user_id] = 2 |
|
548 |
get :download_all, :params => { |
|
549 |
:object_type => 'issues', |
|
550 |
:object_id => '2' |
|
551 |
} |
|
552 |
assert_response 200 |
|
553 |
assert_equal response.headers['Content-Type'], 'application/zip' |
|
554 |
assert_match (/issue-2-attachments.zip/), response.headers['Content-Disposition'] |
|
555 |
assert_not_includes Dir.entries(Rails.root.join('tmp')), /attachments_zip/ |
|
556 |
end |
|
557 | ||
558 |
def test_download_all_with_invalid_container |
|
559 |
@request.session[:user_id] = 2 |
|
560 |
get :download_all, :params => { |
|
561 |
:object_type => 'issues', |
|
562 |
:object_id => '999' |
|
563 |
} |
|
564 |
assert_response 404 |
|
565 |
end |
|
566 | ||
567 |
def test_download_all_without_readable_attachments |
|
568 |
@request.session[:user_id] = 2 |
|
569 |
get :download_all, :params => { |
|
570 |
:object_type => 'issues', |
|
571 |
:object_id => '1' |
|
572 |
} |
|
573 |
assert_equal Issue.find(1).attachments, [] |
|
574 |
assert_response 404 |
|
575 |
end |
|
576 | ||
577 |
def test_download_all_with_maximum_bulk_download_size_larger_than_attachments |
|
578 |
Redmine::Configuration.with 'bulk_download_max_size' => 0 do |
|
579 |
@request.session[:user_id] = 2 |
|
580 |
get :download_all, :params => { |
|
581 |
:object_type => 'issues', |
|
582 |
:object_id => '2', |
|
583 |
:back_url => '/issues/2' |
|
584 |
} |
|
585 |
assert_redirected_to '/issues/2' |
|
586 |
assert_equal flash[:error], 'These attachments cannot be bulk download because it exceeds the maximum allowed bulk download size (0)' |
|
587 |
end |
|
588 |
end |
|
589 | ||
546 | 590 |
def test_destroy_issue_attachment |
547 | 591 |
set_tmp_attachments_directory |
548 | 592 |
issue = Issue.find(3) |
test/unit/attachment_test.rb | ||
---|---|---|
278 | 278 |
end |
279 | 279 |
end |
280 | 280 | |
281 |
def test_attachments_to_zip_with_attachments |
|
282 |
attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1) |
|
283 |
Tempfile.create('attachments_zip', Rails.root.join('tmp')) do |tempfile| |
|
284 |
zip_file = Attachment.attachments_to_zip(tempfile, [attachment]) |
|
285 |
assert_instance_of File, zip_file |
|
286 |
end |
|
287 |
end |
|
288 | ||
289 |
def test_attachments_to_zip_without_attachments |
|
290 |
Tempfile.create('attachments_zip', Rails.root.join('tmp')) do |tempfile| |
|
291 |
zip_file = Attachment.attachments_to_zip(tempfile, []) |
|
292 |
assert_nil zip_file |
|
293 |
end |
|
294 |
end |
|
295 | ||
296 |
def test_attachments_to_zip_should_not_duplicate_file_names |
|
297 |
attachment_1 = Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1) |
|
298 |
attachment_2 = Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1) |
|
299 |
Tempfile.create('attachments_zip', Rails.root.join('tmp')) do |tempfile| |
|
300 |
zip_file = Attachment.attachments_to_zip(tempfile, [attachment_1, attachment_2]) |
|
301 |
zip_file_names = ['testfile.txt', 'testfile(2).txt'] |
|
302 | ||
303 |
Zip::File.open(zip_file.path) do |z| |
|
304 |
z.each_with_index do |entry, i| |
|
305 |
assert_includes zip_file_names[i], entry.name |
|
306 |
end |
|
307 |
end |
|
308 |
end |
|
309 |
end |
|
310 | ||
281 | 311 |
def test_move_from_root_to_target_directory_should_move_root_files |
282 | 312 |
a = Attachment.find(20) |
283 | 313 |
assert a.disk_directory.blank? |