Commit 974eed2d authored by Micaël Bergeron's avatar Micaël Bergeron

add specs around the migration code

parent fcff2995
...@@ -10,17 +10,6 @@ class AvatarUploader < GitlabUploader ...@@ -10,17 +10,6 @@ class AvatarUploader < GitlabUploader
model.avatar.file && model.avatar.file.present? model.avatar.file && model.avatar.file.present?
end end
# We set move_to_store and move_to_cache to 'false' to prevent stealing
# the avatar file from a project when forking it.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/26158
def move_to_store
false
end
def move_to_cache
false
end
private private
def dynamic_segment def dynamic_segment
......
...@@ -32,12 +32,12 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -32,12 +32,12 @@ class GitlabUploader < CarrierWave::Uploader::Base
# Reduce disk IO # Reduce disk IO
def move_to_cache def move_to_cache
true super || true
end end
# Reduce disk IO # Reduce disk IO
def move_to_store def move_to_store
true super || true
end end
def exists? def exists?
......
...@@ -68,6 +68,7 @@ module ObjectStorage ...@@ -68,6 +68,7 @@ module ObjectStorage
base.include(ObjectStorage) base.include(ObjectStorage)
before :store, :verify_license! before :store, :verify_license!
after :migrate, :delete_migrated_file
end end
class_methods do class_methods do
...@@ -155,26 +156,29 @@ module ObjectStorage ...@@ -155,26 +156,29 @@ module ObjectStorage
return unless object_store != new_store return unless object_store != new_store
return unless file return unless file
new_file = nil
file_to_delete = file file_to_delete = file
from_object_store = object_store
self.object_store = new_store # changes the storage and file self.object_store = new_store # changes the storage and file
cache_stored_file! if file_storage? cache_stored_file! if file_storage?
with_callbacks(:migrate, file_to_delete) do
with_callbacks(:store, file_to_delete) do # for #store_versions! with_callbacks(:store, file_to_delete) do # for #store_versions!
storage.store!(file).tap do |new_file| new_file = storage.store!(file)
begin
@file = new_file
persist_object_store! persist_object_store!
file_to_delete.delete if new_file.exists? self.file = new_file
rescue => e
# in case of failure delete new file
new_file.delete
raise e
end
end end
end end
file file
rescue => e
# in case of failure delete new file
new_file.delete unless new_file.nil?
# revert back to the old file
self.object_store = from_object_store
self.file = file_to_delete
raise e
end end
def schedule_migration_to_object_storage(*args) def schedule_migration_to_object_storage(*args)
...@@ -199,15 +203,15 @@ module ObjectStorage ...@@ -199,15 +203,15 @@ module ObjectStorage
end end
def move_to_store def move_to_store
return true if Store::LOCAL false
file.try(:storage) == storage
end end
def move_to_cache def move_to_cache
return true if object_store == Store::LOCAL false
end
file.try(:storage) == cache_storage def delete_migrated_file(migrated_file)
migrated_file.delete if exists?
end end
def verify_license!(_file) def verify_license!(_file)
...@@ -245,6 +249,12 @@ module ObjectStorage ...@@ -245,6 +249,12 @@ module ObjectStorage
private private
# this is a hack around CarrierWave. The #migrate method needs to be
# able to force the current file to the migrated file upon success.
def file=(file)
@file = file
end
def serialization_column def serialization_column
model.class.uploader_options.dig(mounted_as, :mount_on) || mounted_as model.class.uploader_options.dig(mounted_as, :mount_on) || mounted_as
end end
......
...@@ -4,6 +4,78 @@ shared_context 'with storage' do |store, **stub_params| ...@@ -4,6 +4,78 @@ shared_context 'with storage' do |store, **stub_params|
end end
end end
shared_examples "migrates" do | to_store: , from_store: nil |
let(:to) { to_store }
let(:from) { from_store || subject.object_store }
def migrate(to)
subject.migrate!(to)
end
def checksum
Digest::SHA256.hexdigest(subject.read)
end
before do
migrate(from)
end
it 'does nothing when migrating to the current store' do
expect { migrate(from) }.not_to change { subject.object_store }.from(from)
end
it 'migrate to the specified store' do
from_checksum = checksum
expect { migrate(to) }.to change { subject.object_store }.from(from).to(to)
expect(checksum).to eq(from_checksum)
end
it 'removes the original file after the migration' do
original_file = subject.file.path
migrate(to)
expect(File.exist?(original_file)).to be_falsey
end
context 'migration is unsuccessful' do
shared_examples "handles gracefully" do |error:|
it 'does not update the object_store' do
expect { migrate(to) }.to raise_error(error)
expect(subject.object_store).to eq(from)
end
it 'does not delete the original file' do
expect { migrate(to) }.to raise_error(error)
expect(subject.exists?).to be_truthy
end
end
context 'when the store is not supported' do
let(:to) { -1 } # not a valid store
include_examples "handles gracefully", error: ObjectStorage::UnknownStoreError
end
context 'upon a fog failure' do
before do
storage_class = subject.send(:storage_for, to).class
expect_any_instance_of(storage_class).to receive(:store!).and_raise("Store failure.")
end
include_examples "handles gracefully", error: "Store failure."
end
context 'upon a database failure' do
before do
expect(uploader).to receive(:persist_object_store!).and_raise("ActiveRecord failure.")
end
include_examples "handles gracefully", error: "ActiveRecord failure."
end
end
end
shared_examples "matches the method pattern" do |method| shared_examples "matches the method pattern" do |method|
let(:target) { subject } let(:target) { subject }
let(:args) { nil } let(:args) { nil }
......
require 'spec_helper' require 'spec_helper'
describe AttachmentUploader do describe AttachmentUploader do
let(:uploader) { described_class.new(build_stubbed(:user), :attachment) } let(:note) { create(:note, :with_attachment) }
let(:uploader) { note.attachment }
let(:upload) { create(:upload, :attachment_upload, model: uploader.model) } let(:upload) { create(:upload, :attachment_upload, model: uploader.model) }
subject { uploader } subject { uploader }
it_behaves_like 'builds correct paths', it_behaves_like 'builds correct paths',
store_dir: %r[uploads/-/system/user/attachment/], store_dir: %r[uploads/-/system/note/attachment/],
upload_path: %r[uploads/-/system/user/attachment/], upload_path: %r[uploads/-/system/note/attachment/],
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/user/attachment/] absolute_path: %r[#{CarrierWave.root}/uploads/-/system/note/attachment/]
describe '#move_to_cache' do
it 'is true' do
expect(uploader.move_to_cache).to eq(true)
end
end
describe '#move_to_store' do
it 'is true' do
expect(uploader.move_to_store).to eq(true)
end
end
# EE-specific # EE-specific
context "object_store is REMOTE" do context "object_store is REMOTE" do
...@@ -32,7 +21,17 @@ describe AttachmentUploader do ...@@ -32,7 +21,17 @@ describe AttachmentUploader do
include_context 'with storage', described_class::Store::REMOTE include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths', it_behaves_like 'builds correct paths',
store_dir: %r[user/attachment/], store_dir: %r[note/attachment/],
upload_path: %r[user/attachment/] upload_path: %r[note/attachment/]
end
describe "#migrate!" do
before do
uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt')))
stub_uploads_object_storage
end
it_behaves_like "migrates", to_store: described_class::Store::REMOTE
it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
end end
end end
...@@ -12,18 +12,6 @@ describe AvatarUploader do ...@@ -12,18 +12,6 @@ describe AvatarUploader do
upload_path: %r[uploads/-/system/user/avatar/], upload_path: %r[uploads/-/system/user/avatar/],
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/user/avatar/] absolute_path: %r[#{CarrierWave.root}/uploads/-/system/user/avatar/]
describe '#move_to_cache' do
it 'is false' do
expect(uploader.move_to_cache).to eq(false)
end
end
describe '#move_to_store' do
it 'is false' do
expect(uploader.move_to_store).to eq(false)
end
end
# EE-specific # EE-specific
context "object_store is REMOTE" do context "object_store is REMOTE" do
before do before do
...@@ -36,4 +24,17 @@ describe AvatarUploader do ...@@ -36,4 +24,17 @@ describe AvatarUploader do
store_dir: %r[user/avatar/], store_dir: %r[user/avatar/],
upload_path: %r[user/avatar/] upload_path: %r[user/avatar/]
end end
context "with a file" do
let(:project) { create(:project, :with_avatar) }
let(:uploader) { project.avatar }
let(:upload) { uploader.upload }
before do
stub_uploads_object_storage
end
it_behaves_like "migrates", to_store: described_class::Store::REMOTE
it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
end
end end
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe FileUploader do describe FileUploader do
let(:group) { create(:group, name: 'awesome') } let(:group) { create(:group, name: 'awesome') }
let(:project) { build_stubbed(:project, namespace: group, name: 'project') } let(:project) { create(:project, namespace: group, name: 'project') }
let(:uploader) { described_class.new(project) } let(:uploader) { described_class.new(project) }
let(:upload) { double(model: project, path: 'secret/foo.jpg') } let(:upload) { double(model: project, path: 'secret/foo.jpg') }
...@@ -17,7 +17,7 @@ describe FileUploader do ...@@ -17,7 +17,7 @@ describe FileUploader do
shared_examples 'uses hashed storage' do shared_examples 'uses hashed storage' do
context 'when rolled out attachments' do context 'when rolled out attachments' do
before do before do
expect(project).to receive(:disk_path).and_return('ca/fe/fe/ed') allow(project).to receive(:disk_path).and_return('ca/fe/fe/ed')
end end
let(:project) { build_stubbed(:project, :hashed, namespace: group, name: 'project') } let(:project) { build_stubbed(:project, :hashed, namespace: group, name: 'project') }
...@@ -66,15 +66,13 @@ describe FileUploader do ...@@ -66,15 +66,13 @@ describe FileUploader do
end end
end end
describe '#move_to_cache' do describe "#migrate!" do
it 'is true' do before do
expect(uploader.move_to_cache).to eq(true) uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')))
end stub_uploads_object_storage
end end
describe '#move_to_store' do it_behaves_like "migrates", to_store: described_class::Store::REMOTE
it 'is true' do it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
expect(uploader.move_to_store).to eq(true)
end
end end
end end
...@@ -23,18 +23,6 @@ describe LfsObjectUploader do ...@@ -23,18 +23,6 @@ describe LfsObjectUploader do
store_dir: %r[\h{2}/\h{2}] store_dir: %r[\h{2}/\h{2}]
end end
describe '#move_to_cache' do
it 'is true' do
expect(uploader.move_to_cache).to eq(true)
end
end
describe '#move_to_store' do
it 'is true' do
expect(uploader.move_to_store).to eq(true)
end
end
describe 'migration to object storage' do describe 'migration to object storage' do
context 'with object storage disabled' do context 'with object storage disabled' do
it "is skipped" do it "is skipped" do
......
...@@ -26,4 +26,14 @@ describe NamespaceFileUploader do ...@@ -26,4 +26,14 @@ describe NamespaceFileUploader do
store_dir: %r[namespace/\d+/\h+], store_dir: %r[namespace/\d+/\h+],
upload_path: IDENTIFIER upload_path: IDENTIFIER
end end
describe "#migrate!" do
before do
uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt')))
stub_uploads_object_storage
end
it_behaves_like "migrates", to_store: described_class::Store::REMOTE
it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
end
end end
...@@ -83,7 +83,7 @@ describe ObjectStorage do ...@@ -83,7 +83,7 @@ describe ObjectStorage do
expect(object).to receive(:file_store).and_return(described_class::Store::REMOTE) expect(object).to receive(:file_store).and_return(described_class::Store::REMOTE)
end end
it "returns given value" do it "returns the given value" do
expect(uploader.object_store).to eq(described_class::Store::REMOTE) expect(uploader.object_store).to eq(described_class::Store::REMOTE)
end end
end end
...@@ -143,11 +143,6 @@ describe ObjectStorage do ...@@ -143,11 +143,6 @@ describe ObjectStorage do
it "uploader include described_class::Concern" do it "uploader include described_class::Concern" do
expect(uploader).to be_a(described_class::Concern) expect(uploader).to be_a(described_class::Concern)
end end
it 'moves files locally' do
expect(uploader.move_to_store).to be(true)
expect(uploader.move_to_cache).to be(true)
end
end end
describe '#use_file' do describe '#use_file' do
......
...@@ -43,4 +43,14 @@ describe PersonalFileUploader do ...@@ -43,4 +43,14 @@ describe PersonalFileUploader do
) )
end end
end end
describe "#migrate!" do
before do
uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt')))
stub_uploads_object_storage
end
it_behaves_like "migrates", to_store: described_class::Store::REMOTE
it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
end
end end
...@@ -59,13 +59,13 @@ describe RecordsUploads do ...@@ -59,13 +59,13 @@ describe RecordsUploads do
it "does not create an Upload record when the file doesn't exist" do it "does not create an Upload record when the file doesn't exist" do
allow(uploader).to receive(:file).and_return(double(exists?: false)) allow(uploader).to receive(:file).and_return(double(exists?: false))
expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.to_not change { Upload.count } expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.not_to change { Upload.count }
end end
it 'does not create an Upload record if model is missing' do it 'does not create an Upload record if model is missing' do
allow_any_instance_of(RecordsUploadsExampleUploader).to receive(:model).and_return(nil) allow_any_instance_of(RecordsUploadsExampleUploader).to receive(:model).and_return(nil)
expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.to_not change { Upload.count } expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.not_to change { Upload.count }
end end
it 'it destroys Upload records at the same path before recording' do it 'it destroys Upload records at the same path before recording' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment