Project

General

Profile

Feature #7056 » compress_the_all_attachments_in_issue_v2.patch

Mizuki ISHIKAWA, 2018-12-25 03:31

View differences:

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?
(2-2/22)