diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 3e761714e53..f8f8da7af96 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -52,6 +52,9 @@ class Attachment < ActiveRecord::Base after_commit :extract_fulltext, on: :create + after_create :schedule_cleanup_uncontainered_job, + unless: :containered? + ## # Returns an URL if the attachment is stored in an external (fog) attachment storage # or nil otherwise. @@ -121,6 +124,10 @@ class Attachment < ActiveRecord::Base file.readable? end + def containered? + container.present? + end + def diskfile file.local_file end @@ -180,6 +187,10 @@ class Attachment < ActiveRecord::Base private + def schedule_cleanup_uncontainered_job + Delayed::Job::enqueue Attachments::CleanupUncontaineredJob.new + end + def filesize_below_allowed_maximum if filesize > Setting.attachment_max_size.to_i.kilobytes errors.add(:file, :file_too_large, count: Setting.attachment_max_size.to_i.kilobytes) diff --git a/app/workers/attachments/cleanup_uncontainered_job.rb b/app/workers/attachments/cleanup_uncontainered_job.rb new file mode 100644 index 00000000000..3a9bf6a1f7e --- /dev/null +++ b/app/workers/attachments/cleanup_uncontainered_job.rb @@ -0,0 +1,48 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# 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. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +class Attachments::CleanupUncontaineredJob < ApplicationJob + def perform + Attachment + .where(container: nil) + .where(too_old) + .destroy_all + end + + private + + def too_old + attachment_table = Attachment.arel_table + + attachment_table[:created_at] + .lteq(Time.now - OpenProject::Configuration.attachments_grace_period.minutes) + .to_sql + end +end diff --git a/config/configuration.yml.example b/config/configuration.yml.example index 7a020bf9647..095cdaba696 100644 --- a/config/configuration.yml.example +++ b/config/configuration.yml.example @@ -186,6 +186,10 @@ default: # attachments_storage_path: /var/openproject/files # attachments_storage_path: + # Grace period until uploaded but unassigned (i.e. to a container like work packages, + # wiki pages) attachments are deleted (in minutes) + # attachment_grace_period: 180 + # Configuration of the autologin cookie. # autologin_cookie_name: the name of the cookie (default: autologin) # autologin_cookie_path: the cookie path (default: /) diff --git a/lib/open_project/configuration.rb b/lib/open_project/configuration.rb index 6af076cd3c0..791abddd676 100644 --- a/lib/open_project/configuration.rb +++ b/lib/open_project/configuration.rb @@ -39,6 +39,7 @@ module OpenProject @defaults = { 'attachments_storage' => 'file', 'attachments_storage_path' => nil, + 'attachments_grace_period' => 180, 'autologin_cookie_name' => 'autologin', 'autologin_cookie_path' => '/', 'autologin_cookie_secure' => false, @@ -457,15 +458,15 @@ module OpenProject def define_config_methods @config.keys.each do |setting| - (class << self; self; end).class_eval do - define_method setting do - self[setting] - end + next if respond_to? setting - define_method "#{setting}?" do - ['true', true, '1'].include? self[setting] - end - end unless respond_to? setting + define_singleton_method setting do + self[setting] + end + + define_singleton_method "#{setting}?" do + ['true', true, '1'].include? self[setting] + end end end end diff --git a/spec/models/attachment_spec.rb b/spec/models/attachment_spec.rb index 29e3e4f3cdc..01ab957ea47 100644 --- a/spec/models/attachment_spec.rb +++ b/spec/models/attachment_spec.rb @@ -134,6 +134,20 @@ describe Attachment, type: :model do end end + describe '#containered?' do + it 'is false if the attachment has no container' do + stubbed_attachment.container = nil + + expect(stubbed_attachment) + .not_to be_containered + end + + it 'is true if the attachment has a container' do + expect(stubbed_attachment) + .to be_containered + end + end + describe 'create' do it('creates a jpg file called test') do expect(File.exists?(attachment.diskfile.path)).to eq true @@ -160,6 +174,29 @@ describe Attachment, type: :model do expect(attachment.digest) .to eql Digest::MD5.file(file.path).hexdigest end + + it 'adds no cleanup job' do + expect(Delayed::Job) + .not_to receive(:enqueue) + .with an_instance_of(Attachments::CleanupUncontaineredJob) + + attachment.save! + end + + context 'with an unclaimed attachment' do + let(:container) { nil } + + it 'adds a cleanup job' do + allow(Delayed::Job) + .to receive(:enqueue) + + expect(Delayed::Job) + .to receive(:enqueue) + .with an_instance_of(Attachments::CleanupUncontaineredJob) + + attachment.save! + end + end end describe 'two attachments with same file name' do diff --git a/spec/workers/attachments/cleanup_uncontainered_job_integration_spec.rb b/spec/workers/attachments/cleanup_uncontainered_job_integration_spec.rb new file mode 100644 index 00000000000..40ea6ac84e1 --- /dev/null +++ b/spec/workers/attachments/cleanup_uncontainered_job_integration_spec.rb @@ -0,0 +1,56 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# 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. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +describe Attachments::CleanupUncontaineredJob, type: :job do + let(:grace_period) { 120 } + let!(:containered_attachment) { FactoryBot.create(:attachment) } + let!(:old_uncontainered_attachment) do + FactoryBot.create(:attachment, container: nil, created_at: Time.now - grace_period.minutes) + end + let!(:new_uncontainered_attachment) do + FactoryBot.create(:attachment, container: nil, created_at: Time.now - (grace_period - 1).minutes) + end + let(:job) { described_class.new } + + before do + allow(OpenProject::Configuration) + .to receive(:attachments_grace_period) + .and_return(grace_period) + end + + it 'removes all uncontainered attachments that are older than the grace period' do + job.perform + + expect(Attachment.all) + .to match_array([containered_attachment, new_uncontainered_attachment]) + end +end