Feature #7056 » compress_the_all_attachments_in_issue.patch
| Gemfile | ||
|---|---|---|
| 14 | 14 |
gem "mimemagic" |
| 15 | 15 |
gem "mail", "~> 2.6.4" |
| 16 | 16 |
gem "csv", "~> 1.0.2" if RUBY_VERSION >= "2.3" |
| 17 |
gem "rubyzip", "~> 1.2.1" |
|
| 17 | 18 | |
| 18 | 19 |
gem "nokogiri", "~> 1.8.0" |
| 19 | 20 |
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] |
|
| 20 | 21 |
before_action :find_editable_attachments, :only => [:edit_all, :update_all] |
| 21 | 22 |
before_action :file_readable, :read_authorize, :only => [:show, :download, :thumbnail] |
| 22 | 23 |
before_action :update_authorize, :only => :update |
| ... | ... | |
| 129 | 130 |
render :action => 'edit_all' |
| 130 | 131 |
end |
| 131 | 132 | |
| 133 |
def download_all |
|
| 134 |
begin |
|
| 135 |
temp_file = Attachment.attachments_to_zip(@container.attachments) |
|
| 136 |
render_404 and return if temp_file.nil? |
|
| 137 |
send_data(File.read(temp_file.path), :type => 'application/zip', :filename => "#{@container.class.to_s.downcase}-#{@container.id}-attachments.zip")
|
|
| 138 |
ensure |
|
| 139 |
# Close and delete the temp file |
|
| 140 |
unless temp_file.nil? |
|
| 141 |
temp_file.close |
|
| 142 |
temp_file.unlink |
|
| 143 |
end |
|
| 144 |
end |
|
| 145 |
end |
|
| 146 | ||
| 132 | 147 |
def update |
| 133 | 148 |
@attachment.safe_attributes = params[:attachment] |
| 134 | 149 |
saved = @attachment.save |
| ... | ... | |
| 190 | 205 |
end |
| 191 | 206 | |
| 192 | 207 |
def find_editable_attachments |
| 208 |
@attachments = @container.attachments.select(&:editable?) |
|
| 209 |
render_404 if @attachments.empty? |
|
| 210 |
end |
|
| 211 | ||
| 212 |
def find_container |
|
| 193 | 213 |
klass = params[:object_type].to_s.singularize.classify.constantize rescue nil |
| 194 | 214 |
unless klass && klass.reflect_on_association(:attachments) |
| 195 | 215 |
render_404 |
| ... | ... | |
| 201 | 221 |
render_403 |
| 202 | 222 |
return |
| 203 | 223 |
end |
| 204 |
@attachments = @container.attachments.select(&:editable?) |
|
| 205 | 224 |
if @container.respond_to?(:project) |
| 206 | 225 |
@project = @container.project |
| 207 | 226 |
end |
| 208 |
render_404 if @attachments.empty? |
|
| 209 | 227 |
rescue ActiveRecord::RecordNotFound |
| 210 | 228 |
render_404 |
| 211 | 229 |
end |
| 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(attachments) |
|
| 338 |
attachments = attachments.select{|attachment| File.readable?(attachment.diskfile) }
|
|
| 339 |
return nil if attachments.blank? |
|
| 340 |
temp_file = Tempfile.new('attachments_zip', Rails.root.join(Attachment.storage_path))
|
|
| 341 |
Zip.unicode_names = true |
|
| 342 |
existing_file_names = [] |
|
| 343 |
Zip::File.open(temp_file.path, Zip::File::CREATE) do |zip| |
|
| 344 |
attachments.each do |attachment| |
|
| 345 |
filename = attachment.filename |
|
| 346 |
existing_file_names << filename |
|
| 347 |
# If a file with the same name already exists, change the file name. |
|
| 348 |
if existing_file_names.count(filename) > 1 |
|
| 349 |
filename = "#{File.basename(filename, ".*")}(#{existing_file_names.count(filename)})#{File.extname(filename)}"
|
|
| 350 |
end |
|
| 351 |
zip.add(filename, attachment.diskfile) |
|
| 352 |
end |
|
| 353 |
end |
|
| 354 |
temp_file |
|
| 355 |
end |
|
| 356 | ||
| 336 | 357 |
# Moves an existing attachment to its target directory |
| 337 | 358 |
def move_to_target_directory! |
| 338 | 359 |
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/locales/en.yml | ||
|---|---|---|
| 988 | 988 |
label_users_visibility_all: All active users |
| 989 | 989 |
label_users_visibility_members_of_visible_projects: Members of visible projects |
| 990 | 990 |
label_edit_attachments: Edit attached files |
| 991 |
label_download_attachments: Download attached files |
|
| 991 | 992 |
label_link_copied_issue: Link copied issue |
| 992 | 993 |
label_ask: Ask |
| 993 | 994 |
label_search_attachments_yes: Search attachment filenames and descriptions |
| 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' |
| test/functional/attachments_controller_test.rb | ||
|---|---|---|
| 540 | 540 |
assert_equal 'This is a Ruby source file', attachment.description |
| 541 | 541 |
end |
| 542 | 542 | |
| 543 |
def test_download_all_with_valid_container |
|
| 544 |
@request.session[:user_id] = 2 |
|
| 545 |
# Check to delete tempfile |
|
| 546 |
Tempfile.any_instance.expects(:close) |
|
| 547 |
Tempfile.any_instance.expects(:unlink) |
|
| 548 | ||
| 549 |
get :download_all, :params => {
|
|
| 550 |
:object_type => 'issues', |
|
| 551 |
:object_id => '2' |
|
| 552 |
} |
|
| 553 |
assert_response 200 |
|
| 554 |
assert_equal response.headers['Content-Type'], 'application/zip' |
|
| 555 |
assert_match (/issue-2-attachments.zip/), response.headers['Content-Disposition'] |
|
| 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 | ||
| 543 | 567 |
def test_destroy_issue_attachment |
| 544 | 568 |
set_tmp_attachments_directory |
| 545 | 569 |
issue = Issue.find(3) |
| test/unit/attachment_test.rb | ||
|---|---|---|
| 277 | 277 |
end |
| 278 | 278 |
end |
| 279 | 279 | |
| 280 |
def test_attachments_to_zip_with_attachments |
|
| 281 |
attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1)
|
|
| 282 |
tempfile = Attachment.attachments_to_zip([attachment]) |
|
| 283 |
assert_instance_of Tempfile, tempfile |
|
| 284 |
end |
|
| 285 | ||
| 286 |
def test_attachments_to_zip_without_attachments |
|
| 287 |
tempfile = Attachment.attachments_to_zip([]) |
|
| 288 |
assert_nil tempfile |
|
| 289 |
end |
|
| 290 | ||
| 291 |
def test_attachments_to_zip_should_not_duplicate_file_names |
|
| 292 |
attachment_1 = Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1)
|
|
| 293 |
attachment_2 = Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1)
|
|
| 294 |
zip_file = Attachment.attachments_to_zip([attachment_1, attachment_2]) |
|
| 295 |
zip_file_names = ['testfile.txt', 'testfile(2).txt'] |
|
| 296 | ||
| 297 |
Zip::File.open(zip_file.path) do |zip_file| |
|
| 298 |
zip_file.each_with_index do |entry, i| |
|
| 299 |
assert_includes zip_file_names[i], entry.name |
|
| 300 |
end |
|
| 301 |
end |
|
| 302 |
end |
|
| 303 | ||
| 280 | 304 |
def test_move_from_root_to_target_directory_should_move_root_files |
| 281 | 305 |
a = Attachment.find(20) |
| 282 | 306 |
assert a.disk_directory.blank? |