kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
273 lines
6.9 KiB
273 lines
6.9 KiB
#-- copyright
|
|
# OpenProject is an open source project management software.
|
|
# Copyright (C) 2012-2022 the OpenProject GmbH
|
|
#
|
|
# 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-2013 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 COPYRIGHT and LICENSE files for more details.
|
|
#++
|
|
|
|
require 'tempfile'
|
|
require 'zip'
|
|
|
|
class BackupJob < ::ApplicationJob
|
|
queue_with_priority :above_normal
|
|
|
|
attr_reader :backup, :user
|
|
|
|
def perform(
|
|
backup:,
|
|
user:,
|
|
include_attachments: Backup.include_attachments?,
|
|
attachment_size_max_sum_mb: Backup.attachment_size_max_sum_mb
|
|
)
|
|
@backup = backup
|
|
@user = user
|
|
@include_attachments = include_attachments
|
|
@attachment_size_max_sum_mb = attachment_size_max_sum_mb
|
|
|
|
run_backup!
|
|
rescue StandardError => e
|
|
failure! error: e.message
|
|
|
|
raise e
|
|
ensure
|
|
after_backup
|
|
end
|
|
|
|
def run_backup!
|
|
@dumped = dump_database! db_dump_file_name # sets error on failure
|
|
|
|
return unless dumped?
|
|
|
|
file_name = create_backup_archive!(
|
|
file_name: archive_file_name,
|
|
db_dump_file_name:
|
|
)
|
|
|
|
store_backup file_name, backup: backup, user: user
|
|
cleanup_previous_backups!
|
|
|
|
notify_backup_ready!
|
|
end
|
|
|
|
def after_backup
|
|
remove_files! db_dump_file_name, archive_file_name
|
|
remove_backup_attachment! unless success?
|
|
|
|
Rails.logger.info(
|
|
"BackupJob(include_attachments: #{include_attachments?}) finished " \
|
|
"with status #{status} " \
|
|
"(dumped: #{dumped?}, archived: #{archived?})"
|
|
)
|
|
end
|
|
|
|
def notify_backup_ready!
|
|
UserMailer.backup_ready(user).deliver_later
|
|
end
|
|
|
|
def dumped?
|
|
@dumped
|
|
end
|
|
|
|
def archived?
|
|
@archived
|
|
end
|
|
|
|
delegate :status, to: :job_status
|
|
|
|
def db_dump_file_name
|
|
@db_dump_file_name ||= tmp_file_name "openproject", ".sql"
|
|
end
|
|
|
|
def archive_file_name
|
|
@archive_file_name ||= tmp_file_name "openproject-backup", ".zip"
|
|
end
|
|
|
|
def status_reference
|
|
arguments.first[:backup]
|
|
end
|
|
|
|
def updates_own_status?
|
|
true
|
|
end
|
|
|
|
def cleanup_previous_backups!
|
|
Backup.where.not(id: backup.id).destroy_all
|
|
end
|
|
|
|
def success?
|
|
job_status.status == JobStatus::Status.statuses[:success]
|
|
end
|
|
|
|
def remove_files!(*files)
|
|
Array(files).each do |file|
|
|
FileUtils.rm_rf file
|
|
end
|
|
end
|
|
|
|
def remove_backup_attachment!
|
|
backup.attachments.each(&:destroy)
|
|
end
|
|
|
|
def store_backup(file_name, backup:, user:)
|
|
File.open(file_name) do |file|
|
|
call = Attachments::CreateService
|
|
.bypass_whitelist(user:)
|
|
.call(container: backup, filename: file_name, file:, description: 'OpenProject backup')
|
|
|
|
call.on_success do
|
|
download_url = ::API::V3::Utilities::PathHelper::ApiV3Path.attachment_content(call.result.id)
|
|
|
|
upsert_status(
|
|
status: :success,
|
|
message: I18n.t('export.succeeded'),
|
|
payload: download_payload(download_url)
|
|
)
|
|
end
|
|
|
|
call.on_failure do
|
|
upsert_status status: :failure,
|
|
message: I18n.t('export.failed', message: call.message)
|
|
end
|
|
end
|
|
end
|
|
|
|
def create_backup_archive!(file_name:, db_dump_file_name:, attachments: attachments_to_include)
|
|
paths_to_clean = []
|
|
clean_up = OpenProject::Configuration.remote_storage?
|
|
|
|
Zip::File.open(file_name, Zip::File::CREATE) do |zipfile|
|
|
attachments.each do |attachment|
|
|
# If an attachment is destroyed on disk, skip i
|
|
diskfile = attachment.diskfile
|
|
next unless diskfile
|
|
|
|
path = diskfile.path
|
|
|
|
zipfile.add "attachment/file/#{attachment.id}/#{attachment[:file]}", path
|
|
|
|
paths_to_clean << get_cache_folder_path(attachment) if clean_up && attachment.file.cached?
|
|
end
|
|
|
|
zipfile.get_output_stream("openproject.sql") { |f| f.write File.read(db_dump_file_name) }
|
|
end
|
|
|
|
remove_paths! paths_to_clean # delete locally cached files that were downloaded just for the backup
|
|
|
|
@archived = true
|
|
|
|
file_name
|
|
end
|
|
|
|
def remove_paths!(paths)
|
|
paths.each do |path|
|
|
FileUtils.rm_rf path
|
|
end
|
|
end
|
|
|
|
def get_cache_folder_path(attachment)
|
|
# expecting paths like /tmp/op_uploaded_files/1639754082-3468-0002-0911/file.ext
|
|
# just making extra sure so we don't delete anything wrong later on
|
|
unless attachment.diskfile.path =~ /#{attachment.file.cache_dir}\/[^\/]+\/[^\/]+/
|
|
raise "Unexpected cache path for attachment ##{attachment.id}: #{attachment.diskfile}"
|
|
end
|
|
|
|
# returning parent as each cached file is in a separate folder which shall be removed too
|
|
Pathname(attachment.diskfile.path).parent.to_s
|
|
end
|
|
|
|
def attachments_to_include
|
|
return Attachment.none if skip_attachments?
|
|
|
|
Backup.attachments_query
|
|
end
|
|
|
|
def skip_attachments?
|
|
!(include_attachments? && Backup.attachments_size_in_bounds?(max: attachment_size_max_sum_mb))
|
|
end
|
|
|
|
def date_tag
|
|
Time.zone.today.iso8601
|
|
end
|
|
|
|
def tmp_file_name(name, ext)
|
|
file = Tempfile.new [name, ext]
|
|
|
|
file.path
|
|
ensure
|
|
file.close
|
|
file.unlink
|
|
end
|
|
|
|
def include_attachments?
|
|
@include_attachments
|
|
end
|
|
|
|
def attachment_size_max_sum_mb
|
|
@attachment_size_max_sum_mb
|
|
end
|
|
|
|
def dump_database!(path)
|
|
_out, err, st = Open3.capture3 pg_env, dump_command(path)
|
|
|
|
failure! error: err unless st.success?
|
|
|
|
st.success?
|
|
end
|
|
|
|
def dump_command(output_file_path)
|
|
"pg_dump -x -O -f '#{output_file_path}'"
|
|
end
|
|
|
|
def failure!(error: nil)
|
|
msg = I18n.t 'backup.failed'
|
|
|
|
upsert_status(
|
|
status: :failure,
|
|
message: error.present? ? "#{msg}: #{error}" : msg
|
|
)
|
|
end
|
|
|
|
def pg_env
|
|
config = ActiveRecord::Base.connection_db_config.configuration_hash
|
|
entries = pg_env_to_connection_config.map do |key, config_key|
|
|
value = config[config_key].to_s
|
|
|
|
[key.to_s, value] if value.present?
|
|
end
|
|
|
|
entries.compact.to_h
|
|
end
|
|
|
|
##
|
|
# Maps the PG env variable name to the key in the AR connection config.
|
|
def pg_env_to_connection_config
|
|
{
|
|
PGHOST: :host,
|
|
PGPORT: :port,
|
|
PGUSER: :username,
|
|
PGPASSWORD: :password,
|
|
PGDATABASE: :database
|
|
}
|
|
end
|
|
end
|
|
|