Commit 11cfef67 authored by Ash McKenzie's avatar Ash McKenzie

Merge branch '207216-lfs-batch-upload-fix' into 'master'

Mark existing LFS object for upload for forks

Closes #207216

See merge request gitlab-org/gitlab!26344
parents cf9d4a9f 1b6cff9f
...@@ -116,6 +116,10 @@ module LfsRequest ...@@ -116,6 +116,10 @@ module LfsRequest
@objects ||= (params[:objects] || []).to_a @objects ||= (params[:objects] || []).to_a
end end
def objects_oids
objects.map { |o| o['oid'].to_s }
end
def has_authentication_ability?(capability) def has_authentication_ability?(capability)
(authentication_abilities || []).include?(capability) (authentication_abilities || []).include?(capability)
end end
......
...@@ -45,15 +45,9 @@ module Repositories ...@@ -45,15 +45,9 @@ module Repositories
params[:operation] == 'upload' params[:operation] == 'upload'
end end
# rubocop: disable CodeReuse/ActiveRecord
def existing_oids
@existing_oids ||= begin
project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def download_objects! def download_objects!
existing_oids = project.all_lfs_objects_oids(oids: objects_oids)
objects.each do |object| objects.each do |object|
if existing_oids.include?(object[:oid]) if existing_oids.include?(object[:oid])
object[:actions] = download_actions(object) object[:actions] = download_actions(object)
...@@ -68,13 +62,17 @@ module Repositories ...@@ -68,13 +62,17 @@ module Repositories
} }
end end
end end
objects objects
end end
def upload_objects! def upload_objects!
existing_oids = project.lfs_objects_oids(oids: objects_oids)
objects.each do |object| objects.each do |object|
object[:actions] = upload_actions(object) unless existing_oids.include?(object[:oid]) object[:actions] = upload_actions(object) unless existing_oids.include?(object[:oid])
end end
objects objects
end end
......
...@@ -1398,12 +1398,17 @@ class Project < ApplicationRecord ...@@ -1398,12 +1398,17 @@ class Project < ApplicationRecord
.where(lfs_objects_projects: { project_id: [self, lfs_storage_project] }) .where(lfs_objects_projects: { project_id: [self, lfs_storage_project] })
end end
# TODO: Call `#lfs_objects` instead once all LfsObjectsProject records are # TODO: Remove this method once all LfsObjectsProject records are backfilled
# backfilled. At that point, projects can look at their own `lfs_objects`. # for forks. At that point, projects can look at their own `lfs_objects` so
# `lfs_objects_oids` can be used instead.
# #
# See https://gitlab.com/gitlab-org/gitlab/issues/122002 for more info. # See https://gitlab.com/gitlab-org/gitlab/issues/122002 for more info.
def lfs_objects_oids def all_lfs_objects_oids(oids: [])
all_lfs_objects.pluck(:oid) oids(all_lfs_objects, oids: oids)
end
def lfs_objects_oids(oids: [])
oids(lfs_objects, oids: oids)
end end
def personal? def personal?
...@@ -2515,6 +2520,12 @@ class Project < ApplicationRecord ...@@ -2515,6 +2520,12 @@ class Project < ApplicationRecord
reset reset
retry retry
end end
def oids(objects, oids: [])
collection = oids.any? ? objects.where(oid: oids) : objects
collection.pluck(:oid)
end
end end
Project.prepend_if_ee('EE::Project') Project.prepend_if_ee('EE::Project')
...@@ -52,7 +52,7 @@ class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker ...@@ -52,7 +52,7 @@ class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker
def link_lfs_objects(source_project, target_project) def link_lfs_objects(source_project, target_project)
Projects::LfsPointers::LfsLinkService Projects::LfsPointers::LfsLinkService
.new(target_project) .new(target_project)
.execute(source_project.lfs_objects_oids) .execute(source_project.all_lfs_objects_oids)
rescue Projects::LfsPointers::LfsLinkService::TooManyOidsError rescue Projects::LfsPointers::LfsLinkService::TooManyOidsError
raise_fork_failure( raise_fork_failure(
source_project, source_project,
......
---
title: Mark existing LFS object for upload for forks
merge_request: 26344
author:
type: fixed
...@@ -5692,6 +5692,53 @@ describe Project do ...@@ -5692,6 +5692,53 @@ describe Project do
end end
end end
describe '#all_lfs_objects_oids' do
let(:project) { create(:project) }
let(:lfs_object) { create(:lfs_object) }
let(:another_lfs_object) { create(:lfs_object) }
subject { project.all_lfs_objects_oids }
context 'when project has associated LFS objects' do
before do
create(:lfs_objects_project, lfs_object: lfs_object, project: project)
create(:lfs_objects_project, lfs_object: another_lfs_object, project: project)
end
it 'returns OIDs of LFS objects' do
expect(subject).to match_array([lfs_object.oid, another_lfs_object.oid])
end
context 'and there are specified oids' do
subject { project.all_lfs_objects_oids(oids: [lfs_object.oid]) }
it 'returns OIDs of LFS objects that match specified oids' do
expect(subject).to eq([lfs_object.oid])
end
end
end
context 'when fork has associated LFS objects to itself and source' do
let(:source) { create(:project) }
let(:project) { fork_project(source) }
before do
create(:lfs_objects_project, lfs_object: lfs_object, project: source)
create(:lfs_objects_project, lfs_object: another_lfs_object, project: project)
end
it 'returns OIDs of LFS objects' do
expect(subject).to match_array([lfs_object.oid, another_lfs_object.oid])
end
end
context 'when project has no associated LFS objects' do
it 'returns empty array' do
expect(subject).to be_empty
end
end
end
describe '#lfs_objects_oids' do describe '#lfs_objects_oids' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:lfs_object) { create(:lfs_object) } let(:lfs_object) { create(:lfs_object) }
...@@ -5708,6 +5755,14 @@ describe Project do ...@@ -5708,6 +5755,14 @@ describe Project do
it 'returns OIDs of LFS objects' do it 'returns OIDs of LFS objects' do
expect(subject).to match_array([lfs_object.oid, another_lfs_object.oid]) expect(subject).to match_array([lfs_object.oid, another_lfs_object.oid])
end end
context 'and there are specified oids' do
subject { project.lfs_objects_oids(oids: [lfs_object.oid]) }
it 'returns OIDs of LFS objects that match specified oids' do
expect(subject).to eq([lfs_object.oid])
end
end
end end
context 'when project has no associated LFS objects' do context 'when project has no associated LFS objects' do
......
...@@ -690,22 +690,34 @@ describe 'Git LFS API and storage' do ...@@ -690,22 +690,34 @@ describe 'Git LFS API and storage' do
end end
context 'when pushing an LFS object that already exists' do context 'when pushing an LFS object that already exists' do
shared_examples_for 'batch upload with existing LFS object' do
it_behaves_like 'LFS http 200 response'
it 'responds with links the object to the project' do
expect(json_response['objects']).to be_kind_of(Array)
expect(json_response['objects'].first).to include(sample_object)
expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
expect(lfs_object.projects.pluck(:id)).to include(other_project.id)
expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))
expect(json_response['objects'].first['actions']['upload']['header']).to include('Content-Type' => 'application/octet-stream')
end
it_behaves_like 'process authorization header', renew_authorization: true
end
let(:update_lfs_permissions) do let(:update_lfs_permissions) do
other_project.lfs_objects << lfs_object other_project.lfs_objects << lfs_object
end end
it_behaves_like 'LFS http 200 response' context 'in another project' do
it_behaves_like 'batch upload with existing LFS object'
it 'responds with links the object to the project' do
expect(json_response['objects']).to be_kind_of(Array)
expect(json_response['objects'].first).to include(sample_object)
expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
expect(lfs_object.projects.pluck(:id)).to include(other_project.id)
expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))
expect(json_response['objects'].first['actions']['upload']['header']).to include('Content-Type' => 'application/octet-stream')
end end
it_behaves_like 'process authorization header', renew_authorization: true context 'in source of fork project' do
let(:project) { fork_project(other_project) }
it_behaves_like 'batch upload with existing LFS object'
end
end end
context 'when pushing a LFS object that does not exist' do context 'when pushing a LFS object that does not exist' do
......
...@@ -83,7 +83,7 @@ describe RepositoryForkWorker do ...@@ -83,7 +83,7 @@ describe RepositoryForkWorker do
it 'calls Projects::LfsPointers::LfsLinkService#execute with OIDs of source project LFS objects' do it 'calls Projects::LfsPointers::LfsLinkService#execute with OIDs of source project LFS objects' do
expect_fork_repository.and_return(true) expect_fork_repository.and_return(true)
expect_next_instance_of(Projects::LfsPointers::LfsLinkService) do |service| expect_next_instance_of(Projects::LfsPointers::LfsLinkService) do |service|
expect(service).to receive(:execute).with(project.lfs_objects_oids) expect(service).to receive(:execute).with(project.all_lfs_objects_oids)
end end
perform! perform!
......
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