Commit e0e66d87 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '13345-conan-file-download-endpoint' into 'master'

Conan file download endpoint

See merge request gitlab-org/gitlab!17798
parents b6555efa dc43147c
# frozen_string_literal: true # frozen_string_literal: true
class Packages::PackageFileFinder class Packages::PackageFileFinder
attr_reader :package, :file_name attr_reader :package, :file_name, :params
def initialize(package, file_name) def initialize(package, file_name, params = {})
@package = package @package = package
@file_name = file_name @file_name = file_name
@params = params
end end
def execute def execute
...@@ -17,9 +18,22 @@ class Packages::PackageFileFinder ...@@ -17,9 +18,22 @@ class Packages::PackageFileFinder
private private
# rubocop: disable CodeReuse/ActiveRecord
def package_files def package_files
package.package_files.where(file_name: file_name) files = package.package_files
files = by_file_name(files)
files = by_conan_file_type(files)
files
end
def by_file_name(files)
files.where(file_name: file_name) # rubocop: disable CodeReuse/ActiveRecord
end
def by_conan_file_type(files)
return files unless params[:conan_file_type]
files.with_conan_file_type(params[:conan_file_type])
end end
# rubocop: enable CodeReuse/ActiveRecord
end end
...@@ -3,6 +3,7 @@ class Packages::PackageFile < ApplicationRecord ...@@ -3,6 +3,7 @@ class Packages::PackageFile < ApplicationRecord
include UpdateProjectStatistics include UpdateProjectStatistics
delegate :project, :project_id, to: :package delegate :project, :project_id, to: :package
delegate :conan_file_type, to: :conan_file_metadatum
update_project_statistics project_statistics_name: :packages_size update_project_statistics project_statistics_name: :packages_size
...@@ -20,6 +21,11 @@ class Packages::PackageFile < ApplicationRecord ...@@ -20,6 +21,11 @@ class Packages::PackageFile < ApplicationRecord
scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) } scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) }
scope :with_conan_file_metadata, -> { includes(:conan_file_metadatum) } scope :with_conan_file_metadata, -> { includes(:conan_file_metadatum) }
scope :with_conan_file_type, ->(file_type) do
joins(:conan_file_metadatum)
.where(packages_conan_file_metadata: { conan_file_type: ::Packages::ConanFileMetadatum.conan_file_types[file_type] })
end
mount_uploader :file, Packages::PackageFileUploader mount_uploader :file, Packages::PackageFileUploader
after_save :update_file_store, if: :saved_change_to_file? after_save :update_file_store, if: :saved_change_to_file?
......
...@@ -66,8 +66,12 @@ class ConanPackagePresenter ...@@ -66,8 +66,12 @@ class ConanPackagePresenter
def map_package_files def map_package_files
package_files.to_a.map do |package_file| package_files.to_a.map do |package_file|
[package_file.file_name, yield(package_file)] key = package_file.file_name
end.to_h.compact value = yield(package_file)
next unless key && value
[key, value]
end.compact.to_h
end end
def package_files def package_files
......
...@@ -20,7 +20,7 @@ module API ...@@ -20,7 +20,7 @@ module API
}.freeze }.freeze
FILE_NAME_REQUIREMENTS = { FILE_NAME_REQUIREMENTS = {
file_name: Gitlab::Regex.conan_file_name_regex file_name: API::NO_SLASH_URL_PART_REGEX
}.freeze }.freeze
PACKAGE_COMPONENT_REGEX = Gitlab::Regex.conan_recipe_component_regex PACKAGE_COMPONENT_REGEX = Gitlab::Regex.conan_recipe_component_regex
...@@ -197,7 +197,7 @@ module API ...@@ -197,7 +197,7 @@ module API
requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel' requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel'
requires :recipe_revision, type: String, desc: 'Conan Recipe Revision' requires :recipe_revision, type: String, desc: 'Conan Recipe Revision'
end end
namespace 'files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision' do namespace 'files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision', requirements: PACKAGE_REQUIREMENTS do
before do before do
authenticate_non_get! authenticate_non_get!
end end
...@@ -205,11 +205,13 @@ module API ...@@ -205,11 +205,13 @@ module API
params do params do
requires :file_name, type: String, desc: 'Package file name' requires :file_name, type: String, desc: 'Package file name'
end end
desc 'Download recipe files' do namespace 'export/:file_name', requirements: FILE_NAME_REQUIREMENTS do
detail 'This feature was introduced in GitLab 12.5' desc 'Download recipe files' do
end detail 'This feature was introduced in GitLab 12.5'
get 'export/:file_name' do end
not_found! get do
download_package_file(:recipe_file)
end
end end
params do params do
...@@ -217,11 +219,13 @@ module API ...@@ -217,11 +219,13 @@ module API
requires :package_revision, type: String, desc: 'Conan Package Revision' requires :package_revision, type: String, desc: 'Conan Package Revision'
requires :file_name, type: String, desc: 'Package file name' requires :file_name, type: String, desc: 'Package file name'
end end
desc 'Download package files' do namespace 'package/:conan_package_reference/:package_revision/:file_name', requirements: FILE_NAME_REQUIREMENTS do
detail 'This feature was introduced in GitLab 12.5' desc 'Download package files' do
end detail 'This feature was introduced in GitLab 12.5'
get 'package/:conan_package_reference/:package_revision/:file_name' do end
not_found! get do
download_package_file(:package_file)
end
end end
end end
end end
...@@ -316,6 +320,15 @@ module API ...@@ -316,6 +320,15 @@ module API
end end
end end
def download_package_file(file_type)
authorize!(:read_package, project)
package_file = ::Packages::PackageFileFinder
.new(package, "#{params[:file_name]}", conan_file_type: file_type).execute!
present_carrierwave_file!(package_file.file)
end
def find_personal_access_token def find_personal_access_token
personal_access_token = find_personal_access_token_from_conan_jwt || personal_access_token = find_personal_access_token_from_conan_jwt ||
find_personal_access_token_from_conan_http_basic_auth find_personal_access_token_from_conan_http_basic_auth
......
...@@ -62,8 +62,8 @@ FactoryBot.define do ...@@ -62,8 +62,8 @@ FactoryBot.define do
create :conan_file_metadatum, :recipe_file, package_file: package_file create :conan_file_metadatum, :recipe_file, package_file: package_file
end end
file { fixture_file_upload('ee/spec/fixtures/conan/recipe_conanfile.py') } file { fixture_file_upload('ee/spec/fixtures/conan/recipe_files/conanfile.py') }
file_name { 'recipe_conanfile.py' } file_name { 'conanfile.py' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' } file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' } file_md5 { '12345abcde' }
file_type { 'py' } file_type { 'py' }
...@@ -75,8 +75,8 @@ FactoryBot.define do ...@@ -75,8 +75,8 @@ FactoryBot.define do
create :conan_file_metadatum, :recipe_file, package_file: package_file create :conan_file_metadatum, :recipe_file, package_file: package_file
end end
file { fixture_file_upload('ee/spec/fixtures/conan/recipe_conanmanifest.txt') } file { fixture_file_upload('ee/spec/fixtures/conan/recipe_files/conanmanifest.txt') }
file_name { 'recipe_conanmanifest.txt' } file_name { 'conanmanifest.txt' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' } file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' } file_md5 { '12345abcde' }
file_type { 'txt' } file_type { 'txt' }
...@@ -88,8 +88,8 @@ FactoryBot.define do ...@@ -88,8 +88,8 @@ FactoryBot.define do
create :conan_file_metadatum, :package_file, package_file: package_file create :conan_file_metadatum, :package_file, package_file: package_file
end end
file { fixture_file_upload('ee/spec/fixtures/conan/package_conanmanifest.txt') } file { fixture_file_upload('ee/spec/fixtures/conan/package_files/conanmanifest.txt') }
file_name { 'package_conanmanifest.txt' } file_name { 'conanmanifest.txt' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' } file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' } file_md5 { '12345abcde' }
file_type { 'txt' } file_type { 'txt' }
...@@ -101,8 +101,8 @@ FactoryBot.define do ...@@ -101,8 +101,8 @@ FactoryBot.define do
create :conan_file_metadatum, :package_file, package_file: package_file create :conan_file_metadatum, :package_file, package_file: package_file
end end
file { fixture_file_upload('ee/spec/fixtures/conan/package_conaninfo.txt') } file { fixture_file_upload('ee/spec/fixtures/conan/package_files/conaninfo.txt') }
file_name { 'package_conaninfo.txt' } file_name { 'conaninfo.txt' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' } file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' } file_md5 { '12345abcde' }
file_type { 'txt' } file_type { 'txt' }
...@@ -114,7 +114,7 @@ FactoryBot.define do ...@@ -114,7 +114,7 @@ FactoryBot.define do
create :conan_file_metadatum, :package_file, package_file: package_file create :conan_file_metadatum, :package_file, package_file: package_file
end end
file { fixture_file_upload('ee/spec/fixtures/conan/conan_package.tgz') } file { fixture_file_upload('ee/spec/fixtures/conan/package_files/conan_package.tgz') }
file_name { 'conan_package.tgz' } file_name { 'conan_package.tgz' }
file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' } file_sha1 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
file_md5 { '12345abcde' } file_md5 { '12345abcde' }
......
...@@ -17,5 +17,17 @@ describe Packages::PackageFileFinder do ...@@ -17,5 +17,17 @@ describe Packages::PackageFileFinder do
expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound) expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound)
end end
context 'with conan_file_type' do
let(:package) { create(:conan_package) }
it 'returns a package of the correct file_type' do
# conan packages contain a conanmanifest.txt file for both conan_file_types
result = described_class.new(package, 'conanmanifest.txt', conan_file_type: :recipe_file).execute!
expect(result.conan_file_type).to eq('recipe_file')
expect(result.conan_file_type).not_to eq('package_file')
end
end
end end
end end
...@@ -10,8 +10,8 @@ RSpec.describe Packages::ConanFileMetadatum, type: :model do ...@@ -10,8 +10,8 @@ RSpec.describe Packages::ConanFileMetadatum, type: :model do
describe 'validations' do describe 'validations' do
let(:package_file) do let(:package_file) do
create(:package_file, create(:package_file,
file: fixture_file_upload('ee/spec/fixtures/conan/recipe_conanfile.py'), file: fixture_file_upload('ee/spec/fixtures/conan/recipe_files/conanfile.py'),
file_name: 'recipe_conanfile.py') file_name: 'conanfile.py')
end end
it { is_expected.to validate_presence_of(:package_file) } it { is_expected.to validate_presence_of(:package_file) }
......
...@@ -21,8 +21,8 @@ describe ConanPackagePresenter do ...@@ -21,8 +21,8 @@ describe ConanPackagePresenter do
let(:expected_result) do let(:expected_result) do
{ {
"recipe_conanfile.py" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/recipe_conanfile.py", "conanfile.py" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
"recipe_conanmanifest.txt" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/recipe_conanmanifest.txt" "conanmanifest.txt" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
} }
end end
...@@ -45,8 +45,8 @@ describe ConanPackagePresenter do ...@@ -45,8 +45,8 @@ describe ConanPackagePresenter do
let(:expected_result) do let(:expected_result) do
{ {
"recipe_conanfile.py" => '12345abcde', "conanfile.py" => '12345abcde',
"recipe_conanmanifest.txt" => '12345abcde' "conanmanifest.txt" => '12345abcde'
} }
end end
...@@ -69,8 +69,8 @@ describe ConanPackagePresenter do ...@@ -69,8 +69,8 @@ describe ConanPackagePresenter do
let(:expected_result) do let(:expected_result) do
{ {
"package_conaninfo.txt" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/package_conaninfo.txt", "conaninfo.txt" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
"package_conanmanifest.txt" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/package_conanmanifest.txt", "conanmanifest.txt" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
"conan_package.tgz" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz" "conan_package.tgz" => "#{Settings.build_base_gitlab_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
} }
end end
...@@ -94,8 +94,8 @@ describe ConanPackagePresenter do ...@@ -94,8 +94,8 @@ describe ConanPackagePresenter do
let(:expected_result) do let(:expected_result) do
{ {
"package_conaninfo.txt" => '12345abcde', "conaninfo.txt" => '12345abcde',
"package_conanmanifest.txt" => '12345abcde', "conanmanifest.txt" => '12345abcde',
"conan_package.tgz" => '12345abcde' "conan_package.tgz" => '12345abcde'
} }
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe API::ConanPackages do describe API::ConanPackages do
let_it_be(:package) { create(:conan_package) } let(:package) { create(:conan_package) }
let_it_be(:personal_access_token) { create(:personal_access_token) } let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:user) { personal_access_token.user } let_it_be(:user) { personal_access_token.user }
let(:project) { package.project } let(:project) { package.project }
...@@ -442,68 +442,108 @@ describe API::ConanPackages do ...@@ -442,68 +442,108 @@ describe API::ConanPackages do
context 'file endpoints' do context 'file endpoints' do
let(:jwt) { build_jwt(personal_access_token) } let(:jwt) { build_jwt(personal_access_token) }
let(:headers) { build_auth_headers(jwt.encoded) } let(:headers) { build_auth_headers(jwt.encoded) }
let(:package_file_tgz) { package.package_files.find_by(file_type: 'tgz') } let(:recipe_path) { package.conan_recipe_path }
let(:metadata) { package_file_tgz.conan_file_metadatum }
describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ shared_examples 'denies download with no token' do
:recipe_revision/export/:file_name' do context 'with no private token' do
let(:recipe_path) { package.conan_recipe_path } let(:headers) { {} }
subject do it 'returns 400' do
get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{file_name}"), subject
headers: headers
expect(response).to have_gitlab_http_status(401)
end
end end
end
context 'invalid file' do shared_examples 'a public project with packages' do
let(:file_name) { 'badfile.txt' } it 'returns the file' do
subject
it 'returns 404 not found' do expect(response).to have_gitlab_http_status(200)
subject expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
expect(response).to have_gitlab_http_status(:not_found) shared_examples 'an internal project with packages' do
end before do
project.team.truncate
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end end
context 'valid file' do it_behaves_like 'denies download with no token'
let(:file_name) { package_file_tgz.file_name }
it 'returns forbidden' do it 'returns the file' do
subject subject
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(200)
end expect(response.content_type.to_s).to eq('application/octet-stream')
end end
end end
describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ shared_examples 'a private project with packages' do
:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do before do
let(:recipe_path) { package.conan_recipe_path } project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
subject do it_behaves_like 'denies download with no token'
get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/" \
"#{metadata.conan_package_reference}/#{metadata.package_revision}/#{file_name}"), it 'returns the file' do
headers: headers subject
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq('application/octet-stream')
end end
context 'invalid file' do it 'denies download when not enough permissions' do
let(:file_name) { 'badfile.txt' } project.add_guest(user)
it 'returns 404 not found' do subject
subject
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(403)
end
end end
end
context 'valid file' do shared_examples 'a project is not found' do
let(:file_name) { package_file_tgz.file_name } let(:recipe_path) { 'not/package/for/project' }
it 'returns forbidden' do it 'returns forbidden' do
subject subject
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(403)
end end
end
describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
:recipe_revision/export/:file_name' do
let(:recipe_file) { package.package_files.find_by(file_name: 'conanfile.py') }
let(:metadata) { recipe_file.conan_file_metadatum }
subject do
get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"),
headers: headers
end end
it_behaves_like 'a public project with packages'
it_behaves_like 'an internal project with packages'
it_behaves_like 'a private project with packages'
it_behaves_like 'a project is not found'
end
describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
let(:package_file) { package.package_files.find_by(file_name: 'conaninfo.txt') }
let(:metadata) { package_file.conan_file_metadatum }
subject do
get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"),
headers: headers
end
it_behaves_like 'a public project with packages'
it_behaves_like 'an internal project with packages'
it_behaves_like 'a private project with packages'
it_behaves_like 'a project is not found'
end end
end end
......
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