summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorFrancisco Javier López <fjlopez@gitlab.com>2018-03-30 15:45:59 +0000
committerDouwe Maan <douwe@gitlab.com>2018-03-30 15:45:59 +0000
commit22b05a1ff74d4f64490f93995259602b3d07c3cf (patch)
treee2e1cff25e9e4ab67252b0402cb5df95fdc98d25 /lib
parent7c36e8561c60882e6b0b47c563f7d19f3d6b02a6 (diff)
downloadgitlab-ce-22b05a1ff74d4f64490f93995259602b3d07c3cf.tar.gz
Extend API for exporting a project with direct upload URL
Diffstat (limited to 'lib')
-rw-r--r--lib/api/project_export.rb19
-rw-r--r--lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb83
-rw-r--r--lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb17
-rw-r--r--lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb61
-rw-r--r--lib/gitlab/import_export/after_export_strategy_builder.rb24
-rw-r--r--lib/gitlab/import_export/shared.rb14
6 files changed, 216 insertions, 2 deletions
diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb
index efc4a33ae1b..5ef4e9d530c 100644
--- a/lib/api/project_export.rb
+++ b/lib/api/project_export.rb
@@ -33,11 +33,28 @@ module API
end
params do
optional :description, type: String, desc: 'Override the project description'
+ optional :upload, type: Hash do
+ optional :url, type: String, desc: 'The URL to upload the project'
+ optional :http_method, type: String, default: 'PUT', desc: 'HTTP method to upload the exported project'
+ end
end
post ':id/export' do
project_export_params = declared_params(include_missing: false)
+ after_export_params = project_export_params.delete(:upload) || {}
- user_project.add_export_job(current_user: current_user, params: project_export_params)
+ export_strategy = if after_export_params[:url].present?
+ params = after_export_params.slice(:url, :http_method).symbolize_keys
+
+ Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy.new(params)
+ end
+
+ if export_strategy&.invalid?
+ render_validation_error!(export_strategy)
+ else
+ user_project.add_export_job(current_user: current_user,
+ after_export_strategy: export_strategy,
+ params: project_export_params)
+ end
accepted!
end
diff --git a/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
new file mode 100644
index 00000000000..aef371d81eb
--- /dev/null
+++ b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
@@ -0,0 +1,83 @@
+module Gitlab
+ module ImportExport
+ module AfterExportStrategies
+ class BaseAfterExportStrategy
+ include ActiveModel::Validations
+ extend Forwardable
+
+ StrategyError = Class.new(StandardError)
+
+ AFTER_EXPORT_LOCK_FILE_NAME = '.after_export_action'.freeze
+
+ private
+
+ attr_reader :project, :current_user
+
+ public
+
+ def initialize(attributes = {})
+ @options = OpenStruct.new(attributes)
+
+ self.class.instance_eval do
+ def_delegators :@options, *attributes.keys
+ end
+ end
+
+ def execute(current_user, project)
+ return unless project&.export_project_path
+
+ @project = project
+ @current_user = current_user
+
+ if invalid?
+ log_validation_errors
+
+ return
+ end
+
+ create_or_update_after_export_lock
+ strategy_execute
+
+ true
+ rescue => e
+ project.import_export_shared.error(e)
+ false
+ ensure
+ delete_after_export_lock
+ end
+
+ def to_json(options = {})
+ @options.to_h.merge!(klass: self.class.name).to_json
+ end
+
+ def self.lock_file_path(project)
+ return unless project&.export_path
+
+ File.join(project.export_path, AFTER_EXPORT_LOCK_FILE_NAME)
+ end
+
+ protected
+
+ def strategy_execute
+ raise NotImplementedError
+ end
+
+ private
+
+ def create_or_update_after_export_lock
+ FileUtils.touch(self.class.lock_file_path(project))
+ end
+
+ def delete_after_export_lock
+ lock_file = self.class.lock_file_path(project)
+
+ FileUtils.rm(lock_file) if lock_file.present? && File.exist?(lock_file)
+ end
+
+ def log_validation_errors
+ errors.full_messages.each { |msg| project.import_export_shared.add_error_message(msg) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb b/lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb
new file mode 100644
index 00000000000..4371a7eff56
--- /dev/null
+++ b/lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module ImportExport
+ module AfterExportStrategies
+ class DownloadNotificationStrategy < BaseAfterExportStrategy
+ private
+
+ def strategy_execute
+ notification_service.project_exported(project, current_user)
+ end
+
+ def notification_service
+ @notification_service ||= NotificationService.new
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
new file mode 100644
index 00000000000..938664a95a1
--- /dev/null
+++ b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module ImportExport
+ module AfterExportStrategies
+ class WebUploadStrategy < BaseAfterExportStrategy
+ PUT_METHOD = 'PUT'.freeze
+ POST_METHOD = 'POST'.freeze
+ INVALID_HTTP_METHOD = 'invalid. Only PUT and POST methods allowed.'.freeze
+
+ validates :url, url: true
+
+ validate do
+ unless [PUT_METHOD, POST_METHOD].include?(http_method.upcase)
+ errors.add(:http_method, INVALID_HTTP_METHOD)
+ end
+ end
+
+ def initialize(url:, http_method: PUT_METHOD)
+ super
+ end
+
+ protected
+
+ def strategy_execute
+ handle_response_error(send_file)
+
+ project.remove_exported_project_file
+ end
+
+ def handle_response_error(response)
+ unless response.success?
+ error_code = response.dig('Error', 'Code') || response.code
+ error_message = response.dig('Error', 'Message') || response.message
+
+ raise StrategyError.new("Error uploading the project. Code #{error_code}: #{error_message}")
+ end
+ end
+
+ private
+
+ def send_file
+ export_file = File.open(project.export_project_path)
+
+ Gitlab::HTTP.public_send(http_method.downcase, url, send_file_options(export_file)) # rubocop:disable GitlabSecurity/PublicSend
+ ensure
+ export_file.close if export_file
+ end
+
+ def send_file_options(export_file)
+ {
+ body_stream: export_file,
+ headers: headers
+ }
+ end
+
+ def headers
+ { 'Content-Length' => File.size(project.export_project_path).to_s }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/after_export_strategy_builder.rb b/lib/gitlab/import_export/after_export_strategy_builder.rb
new file mode 100644
index 00000000000..7eabcae2380
--- /dev/null
+++ b/lib/gitlab/import_export/after_export_strategy_builder.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ module ImportExport
+ class AfterExportStrategyBuilder
+ StrategyNotFoundError = Class.new(StandardError)
+
+ def self.build!(strategy_klass, attributes = {})
+ return default_strategy.new unless strategy_klass
+
+ attributes ||= {}
+ klass = strategy_klass.constantize rescue nil
+
+ unless klass && klass < AfterExportStrategies::BaseAfterExportStrategy
+ raise StrategyNotFoundError.new("Strategy #{strategy_klass} not found")
+ end
+
+ klass.new(**attributes.symbolize_keys)
+ end
+
+ def self.default_strategy
+ AfterExportStrategies::DownloadNotificationStrategy
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb
index 3d3d998a6a3..6d7c36ce38b 100644
--- a/lib/gitlab/import_export/shared.rb
+++ b/lib/gitlab/import_export/shared.rb
@@ -22,7 +22,7 @@ module Gitlab
def error(error)
error_out(error.message, caller[0].dup)
- @errors << error.message
+ add_error_message(error.message)
# Debug:
if error.backtrace
@@ -32,6 +32,14 @@ module Gitlab
end
end
+ def add_error_message(error_message)
+ @errors << error_message
+ end
+
+ def after_export_in_progress?
+ File.exist?(after_export_lock_file)
+ end
+
private
def relative_path
@@ -45,6 +53,10 @@ module Gitlab
def error_out(message, caller)
Rails.logger.error("Import/Export error raised on #{caller}: #{message}")
end
+
+ def after_export_lock_file
+ AfterExportStrategies::BaseAfterExportStrategy.lock_file_path(project)
+ end
end
end
end