Commit a5394f8c authored by Stan Hu's avatar Stan Hu

Accelerate uploads via API with Workhorse

Add /api/v4/projects/:id/uploads/authorize endpoint

Relates to https://gitlab.com/gitlab-org/gitlab/-/issues/324813

This enables Workhorse to accelerate API uploads and limits to the
configured maximum attachment size (10 MB by default).
parent 658fef82
---
title: Accelerate uploads via API with Workhorse
merge_request: 57250
author:
type: performance
...@@ -545,10 +545,20 @@ module API ...@@ -545,10 +545,20 @@ module API
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
desc 'Workhorse authorize the file upload' do
detail 'This feature was introduced in GitLab 13.11'
end
post ':id/uploads/authorize', feature_category: :not_owned do
require_gitlab_workhorse!
status 200
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
FileUploader.workhorse_authorize(has_length: false, maximum_size: user_project.max_attachment_size)
end
desc 'Upload a file' desc 'Upload a file'
params do params do
# TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960 requires :file, types: [Rack::Multipart::UploadedFile, ::API::Validations::Types::WorkhorseFile], desc: 'The attachment file to be uploaded'
requires :file, type: File, desc: 'The file to be uploaded' # rubocop:disable Scalability/FileUploads
end end
post ":id/uploads", feature_category: :not_owned do post ":id/uploads", feature_category: :not_owned do
upload = UploadService.new(user_project, params[:file]).execute upload = UploadService.new(user_project, params[:file]).execute
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Upload an attachment', :api, :js do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
let_it_be(:user) { project.owner }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let(:api_path) { "/projects/#{project.id}/uploads" }
let(:url) { capybara_url(api(api_path)) }
let(:file) { fixture_file_upload('spec/fixtures/dk.png') }
subject do
HTTParty.post(
url,
headers: { 'PRIVATE-TOKEN' => personal_access_token.token },
body: { file: file }
)
end
shared_examples 'for an attachment' do
it 'creates package files' do
expect { subject }
.to change { Upload.count }.by(1)
end
it { expect(subject.code).to eq(201) }
end
it_behaves_like 'handling file uploads', 'for an attachment'
end
...@@ -1461,6 +1461,38 @@ RSpec.describe API::Projects do ...@@ -1461,6 +1461,38 @@ RSpec.describe API::Projects do
end end
end end
describe "POST /projects/:id/uploads/authorize" do
include WorkhorseHelpers
let(:headers) { workhorse_internal_api_request_header.merge({ 'HTTP_GITLAB_WORKHORSE' => 1 }) }
context 'with authorized user' do
it "returns 200" do
post api("/projects/#{project.id}/uploads/authorize", user), headers: headers
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['MaximumSize']).to eq(project.max_attachment_size)
end
end
context 'with unauthorized user' do
it "returns 404" do
post api("/projects/#{project.id}/uploads/authorize", user2), headers: headers
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with no Workhorse headers' do
it "returns 403" do
post api("/projects/#{project.id}/uploads/authorize", user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
describe "POST /projects/:id/uploads" do describe "POST /projects/:id/uploads" do
before do before do
project project
......
...@@ -302,6 +302,9 @@ func configureRoutes(u *upstream) { ...@@ -302,6 +302,9 @@ func configureRoutes(u *upstream) {
// Requirements Import via UI upload acceleration // Requirements Import via UI upload acceleration
u.route("POST", projectPattern+`requirements_management/requirements/import_csv`, upload.Accelerate(api, signingProxy, preparers.uploads)), u.route("POST", projectPattern+`requirements_management/requirements/import_csv`, upload.Accelerate(api, signingProxy, preparers.uploads)),
// Uploads via API
u.route("POST", apiProjectPattern+`uploads\z`, upload.Accelerate(api, signingProxy, preparers.uploads)),
// Explicitly proxy API requests // Explicitly proxy API requests
u.route("", apiPattern, proxy), u.route("", apiPattern, proxy),
u.route("", ciAPIPattern, proxy), u.route("", ciAPIPattern, proxy),
......
...@@ -116,6 +116,9 @@ func TestAcceleratedUpload(t *testing.T) { ...@@ -116,6 +116,9 @@ func TestAcceleratedUpload(t *testing.T) {
{"POST", `/example`, false}, {"POST", `/example`, false},
{"POST", `/uploads/personal_snippet`, true}, {"POST", `/uploads/personal_snippet`, true},
{"POST", `/uploads/user`, true}, {"POST", `/uploads/user`, true},
{"POST", `/api/v4/projects/1/uploads`, true},
{"POST", `/api/v4/projects/group%2Fproject/uploads`, true},
{"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/uploads`, true},
{"POST", `/api/v4/projects/1/wikis/attachments`, false}, {"POST", `/api/v4/projects/1/wikis/attachments`, false},
{"POST", `/api/v4/projects/group%2Fproject/wikis/attachments`, false}, {"POST", `/api/v4/projects/group%2Fproject/wikis/attachments`, false},
{"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/wikis/attachments`, false}, {"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/wikis/attachments`, false},
......
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