Skip to content
Snippets Groups Projects
Unverified Commit a2bf1641 authored by James Lopez's avatar James Lopez
Browse files

Update Import/Export to use object storage (based on aa feature flag)

parent b0fa01fc
No related branches found
No related tags found
1 merge request!10495Merge Requests - Assignee
Showing
with 238 additions and 38 deletions
Loading
Loading
@@ -2,6 +2,7 @@ class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
include PreviewMarkdown
include SendFileUpload
 
before_action :whitelist_query_limiting, only: [:create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
Loading
Loading
@@ -188,9 +189,9 @@ class ProjectsController < Projects::ApplicationController
end
 
def download_export
export_project_path = @project.export_project_path
if export_project_path
if export_project_object_storage?
send_upload(@project.import_export_upload.export_file)
elsif export_project_path
send_file export_project_path, disposition: 'attachment'
else
redirect_to(
Loading
Loading
@@ -265,8 +266,6 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json
end
 
private
# Render project landing depending of which features are available
# So if page is not availble in the list it renders the next page
#
Loading
Loading
@@ -424,4 +423,12 @@ class ProjectsController < Projects::ApplicationController
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42440')
end
def export_project_path
@export_project_path ||= @project.export_project_path
end
def export_project_object_storage?
@project.export_project_object_exists?
end
end
class ImportExportUpload < ActiveRecord::Base
include WithUploads
include ObjectStorage::BackgroundMove
belongs_to :project
mount_uploader :import_file, ImportExportUploader
mount_uploader :export_file, ImportExportUploader
def retrieve_upload(_identifier, paths)
Upload.find_by(model: self, path: paths)
end
end
Loading
Loading
@@ -171,6 +171,7 @@ class Project < ActiveRecord::Base
has_one :fork_network, through: :fork_network_member
 
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
 
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
Loading
Loading
@@ -1712,7 +1713,7 @@ class Project < ActiveRecord::Base
:started
elsif after_export_in_progress?
:after_export_action
elsif export_project_path
elsif export_project_path || export_project_object_exists?
:finished
else
:none
Loading
Loading
@@ -1727,16 +1728,21 @@ class Project < ActiveRecord::Base
import_export_shared.after_export_in_progress?
end
 
def remove_exports
return nil unless export_path.present?
FileUtils.rm_rf(export_path)
def remove_exports(path = export_path)
if path.present?
FileUtils.rm_rf(path)
elsif export_project_object_exists?
import_export_upload.remove_export_file!
import_export_upload.save
end
end
 
def remove_exported_project_file
return unless export_project_path.present?
remove_exports(export_project_path)
end
 
FileUtils.rm_f(export_project_path)
def export_project_object_exists?
Gitlab::ImportExport.object_storage? && import_export_upload&.export_file&.file
end
 
def full_path_slug
Loading
Loading
Loading
Loading
@@ -10,7 +10,9 @@ class ImportExportCleanUpService
 
def execute
Gitlab::Metrics.measure(:import_export_clean_up) do
next unless File.directory?(path)
clean_up_export_object_files
break unless File.directory?(path)
 
clean_up_export_files
end
Loading
Loading
@@ -21,4 +23,11 @@ class ImportExportCleanUpService
def clean_up_export_files
Gitlab::Popen.popen(%W(find #{path} -not -path #{path} -mmin +#{mmin} -delete))
end
def clean_up_export_object_files
ImportExportUpload.where('updated_at < ?', mmin.minutes.ago).each do |upload|
upload.remove_export_file!
upload.save!
end
end
end
class ImportExportUploader < AttachmentUploader
EXTENSION_WHITELIST = %w[tar.gz].freeze
def extension_whitelist
EXTENSION_WHITELIST
end
def move_to_store
true
end
def move_to_cache
false
end
end
Loading
Loading
@@ -31,7 +31,7 @@
%li Any encrypted tokens
%p
Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.
- if project.export_project_path
- if project.export_status == :finished
= link_to 'Download export', download_export_project_path(project),
rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
= link_to 'Generate new export', generate_new_export_project_path(project),
Loading
Loading
---
title: Add Object Storage to project export
merge_request: 20105
author:
type: added
class CreateImportExportUploads < ActiveRecord::Migration
DOWNTIME = false
def change
create_table :import_export_uploads do |t|
t.datetime_with_timezone :updated_at, null: false
t.references :project, index: true, foreign_key: { on_delete: :cascade }, unique: true
t.text :import_file
t.text :export_file
end
add_index :import_export_uploads, :updated_at
end
end
Loading
Loading
@@ -949,6 +949,16 @@ ActiveRecord::Schema.define(version: 20180702120647) do
 
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
 
create_table "import_export_uploads", force: :cascade do |t|
t.datetime_with_timezone "updated_at", null: false
t.integer "project_id"
t.text "import_file"
t.text "export_file"
end
add_index "import_export_uploads", ["project_id"], name: "index_import_export_uploads_on_project_id", using: :btree
add_index "import_export_uploads", ["updated_at"], name: "index_import_export_uploads_on_updated_at", using: :btree
create_table "internal_ids", id: :bigserial, force: :cascade do |t|
t.integer "project_id"
t.integer "usage", null: false
Loading
Loading
Loading
Loading
@@ -30,5 +30,12 @@ sudo gitlab-rake gitlab:import_export:data
bundle exec rake gitlab:import_export:data RAILS_ENV=production
```
 
In order to enable Object Storage on the Export, you can use the [feature flag][feature-flags]:
```
import_export_object_storage
```
[ce-3050]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3050
[feature-flags]: https://docs.gitlab.com/ee/api/features.html
[tmp]: ../../development/shared_files.md
Loading
Loading
@@ -23,9 +23,13 @@ module API
get ':id/export/download' do
path = user_project.export_project_path
 
render_api_error!('404 Not found or has expired', 404) unless path
present_disk_file!(path, File.basename(path), 'application/gzip')
if path
present_disk_file!(path, File.basename(path), 'application/gzip')
elsif user_project.export_project_object_exists?
present_carrierwave_file!(user_project.import_export_upload.export_file)
else
render_api_error!('404 Not found or has expired', 404)
end
end
 
desc 'Start export' do
Loading
Loading
Loading
Loading
@@ -40,6 +40,10 @@ module Gitlab
"#{basename[0..FILENAME_LIMIT]}_export.tar.gz"
end
 
def object_storage?
Feature.enabled?(:import_export_object_storage)
end
def version
VERSION
end
Loading
Loading
Loading
Loading
@@ -24,9 +24,10 @@ module Gitlab
end
 
def execute(current_user, project)
return unless project&.export_project_path
@project = project
return unless @project.export_status == :finished
@current_user = current_user
 
if invalid?
Loading
Loading
@@ -51,9 +52,12 @@ module Gitlab
end
 
def self.lock_file_path(project)
return unless project&.export_path
return unless project.export_path || object_storage?
 
File.join(project.export_path, AFTER_EXPORT_LOCK_FILE_NAME)
lock_path = project.import_export_shared.archive_path
FileUtils.mkdir_p(lock_path)
File.join(lock_path, AFTER_EXPORT_LOCK_FILE_NAME)
end
 
protected
Loading
Loading
@@ -77,6 +81,10 @@ module Gitlab
def log_validation_errors
errors.full_messages.each { |msg| project.import_export_shared.add_error_message(msg) }
end
def object_storage?
project.export_project_object_exists?
end
end
end
end
Loading
Loading
Loading
Loading
@@ -38,14 +38,20 @@ module Gitlab
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
Gitlab::HTTP.public_send(http_method.downcase, url, send_file_options) # rubocop:disable GitlabSecurity/PublicSend
ensure
export_file.close if export_file
export_file.close if export_file && !object_storage?
end
def export_file
if object_storage?
project.import_export_upload.export_file.file.open
else
File.open(project.export_project_path)
end
end
 
def send_file_options(export_file)
def send_file_options
{
body_stream: export_file,
headers: headers
Loading
Loading
@@ -53,7 +59,15 @@ module Gitlab
end
 
def headers
{ 'Content-Length' => File.size(project.export_project_path).to_s }
{ 'Content-Length' => export_size.to_s }
end
def export_size
if object_storage?
project.import_export_upload.export_file.file.size
else
File.size(project.export_project_path)
end
end
end
end
Loading
Loading
Loading
Loading
@@ -15,15 +15,22 @@ module Gitlab
def save
if compress_and_save
remove_export_path
Rails.logger.info("Saved project export #{archive_file}")
archive_file
save_on_object_storage if use_object_storage?
else
@shared.error(Gitlab::ImportExport::Error.new("Unable to save #{archive_file} into #{@shared.export_path}"))
@shared.error(Gitlab::ImportExport::Error.new(error_message))
false
end
rescue => e
@shared.error(e)
false
ensure
if use_object_storage?
remove_archive
remove_export_path
end
end
 
private
Loading
Loading
@@ -36,9 +43,29 @@ module Gitlab
FileUtils.rm_rf(@shared.export_path)
end
 
def remove_archive
FileUtils.rm_rf(@shared.archive_path)
end
def archive_file
@archive_file ||= File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(project: @project))
end
def save_on_object_storage
upload = ImportExportUpload.find_or_initialize_by(project: @project)
File.open(archive_file) { |file| upload.export_file = file }
upload.save!
end
def use_object_storage?
Gitlab::ImportExport.object_storage?
end
def error_message
"Unable to save #{archive_file} into #{@shared.export_path}. Object storage enabled: #{use_object_storage?}"
end
end
end
end
Loading
Loading
@@ -790,23 +790,55 @@ describe ProjectsController do
project.add_master(user)
end
 
context 'when project export is enabled' do
it 'returns 302' do
get :download_export, namespace_id: project.namespace, id: project
context 'object storage disabled' do
before do
stub_feature_flags(import_export_object_storage: false)
end
 
expect(response).to have_gitlab_http_status(302)
context 'when project export is enabled' do
it 'returns 302' do
get :download_export, namespace_id: project.namespace, id: project
expect(response).to have_gitlab_http_status(302)
end
end
context 'when project export is disabled' do
before do
stub_application_setting(project_export_enabled?: false)
end
it 'returns 404' do
get :download_export, namespace_id: project.namespace, id: project
expect(response).to have_gitlab_http_status(404)
end
end
end
 
context 'when project export is disabled' do
context 'object storage enabled' do
before do
stub_application_setting(project_export_enabled?: false)
stub_feature_flags(import_export_object_storage: true)
end
 
it 'returns 404' do
get :download_export, namespace_id: project.namespace, id: project
context 'when project export is enabled' do
it 'returns 302' do
get :download_export, namespace_id: project.namespace, id: project
 
expect(response).to have_gitlab_http_status(404)
expect(response).to have_gitlab_http_status(302)
end
end
context 'when project export is disabled' do
before do
stub_application_setting(project_export_enabled?: false)
end
it 'returns 404' do
get :download_export, namespace_id: project.namespace, id: project
expect(response).to have_gitlab_http_status(404)
end
end
end
end
Loading
Loading
FactoryBot.define do
factory :import_export_upload do
project { create(:project) }
end
end
Loading
Loading
@@ -103,6 +103,22 @@ FactoryBot.define do
end
 
trait :with_export do
before(:create) do |_project, _evaluator|
allow(Feature).to receive(:enabled?).with(:import_export_object_storage) { false }
allow(Feature).to receive(:enabled?).with('import_export_object_storage') { false }
end
after(:create) do |project, _evaluator|
ProjectExportWorker.new.perform(project.creator.id, project.id)
end
end
trait :with_object_export do
before(:create) do |_project, _evaluator|
allow(Feature).to receive(:enabled?).with(:import_export_object_storage) { true }
allow(Feature).to receive(:enabled?).with('import_export_object_storage') { true }
end
after(:create) do |project, evaluator|
ProjectExportWorker.new.perform(project.creator.id, project.id)
end
Loading
Loading
Loading
Loading
@@ -25,6 +25,7 @@ describe 'Import/Export - project export integration test', :js do
 
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
stub_feature_flags(import_export_object_storage: false)
end
 
after do
Loading
Loading
Loading
Loading
@@ -5,6 +5,7 @@ describe 'Import/Export - Namespace export file cleanup', :js do
 
before do
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
stub_feature_flags(import_export_object_storage: false)
end
 
after do
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment