Commit 401f65c4 authored by Steve Azzopardi's avatar Steve Azzopardi

Add endpoint to download single artifact by ref

Add a new endpoint
`projects/:id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name`
which is the close the web URL for consistency sake. This endpoint can
be used to download a single file from artifacts for the specified ref
and job.

closes https://gitlab.com/gitlab-org/gitlab-ce/issues/54626
parent 62d97112
...@@ -649,6 +649,11 @@ class Project < ActiveRecord::Base ...@@ -649,6 +649,11 @@ class Project < ActiveRecord::Base
end end
end end
def latest_successful_build_for(job_name, ref = default_branch)
builds = latest_successful_builds_for(ref)
builds.find_by!(name: job_name)
end
def merge_base_commit(first_commit_id, second_commit_id) def merge_base_commit(first_commit_id, second_commit_id)
sha = repository.merge_base(first_commit_id, second_commit_id) sha = repository.merge_base(first_commit_id, second_commit_id)
commit_by(oid: sha) if sha commit_by(oid: sha) if sha
......
---
title: Add new endpoint to download single artifact file for a ref
merge_request: 23538
author:
type: added
...@@ -404,7 +404,7 @@ Example response: ...@@ -404,7 +404,7 @@ Example response:
[ce-5347]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5347 [ce-5347]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5347
## Download a single artifact file ## Download a single artifact file by job ID
> Introduced in GitLab 10.0 > Introduced in GitLab 10.0
...@@ -438,6 +438,41 @@ Example response: ...@@ -438,6 +438,41 @@ Example response:
| 400 | Invalid path provided | | 400 | Invalid path provided |
| 404 | Build not found or no file/artifacts | | 404 | Build not found or no file/artifacts |
## Download a single artifact file from specific tag or branch
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23538) in GitLab 11.5.
Download a single artifact file from a specific tag or branch from within the
job's artifacts archive. The file is extracted from the archive and streamed to
the client.
```
GET /projects/:id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name
```
Parameters:
| Attribute | Type | Required | Description |
|-----------------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `ref_name` | string | yes | Branch or tag name in repository. HEAD or SHA references are not supported. |
| `artifact_path` | string | yes | Path to a file inside the artifacts archive. |
| `job` | string | yes | The name of the job. |
Example request:
```sh
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/raw/some/release/file.pdf?job=pdf"
```
Possible response status codes:
| Status | Description |
|-----------|--------------------------------------|
| 200 | Sends a single artifact file |
| 400 | Invalid path provided |
| 404 | Build not found or no file/artifacts |
## Get a trace file ## Get a trace file
Get a trace of a specific job of a project Get a trace of a specific job of a project
......
...@@ -35,6 +35,29 @@ module API ...@@ -35,6 +35,29 @@ module API
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
desc 'Download a specific file from artifacts archive from a ref' do
detail 'This feature was introduced in GitLab 11.5'
end
params do
requires :ref_name, type: String, desc: 'The ref from repository'
requires :job, type: String, desc: 'The name for the job'
requires :artifact_path, type: String, desc: 'Artifact path'
end
get ':id/jobs/artifacts/:ref_name/raw/*artifact_path',
format: false,
requirements: { ref_name: /.+/ } do
authorize_download_artifacts!
build = user_project.latest_successful_build_for(params[:job], params[:ref_name])
path = Gitlab::Ci::Build::Artifacts::Path
.new(params[:artifact_path])
bad_request! unless path.valid?
send_artifacts_entry(build, path)
end
desc 'Download the artifacts archive from a job' do desc 'Download the artifacts archive from a job' do
detail 'This feature was introduced in GitLab 8.5' detail 'This feature was introduced in GitLab 8.5'
end end
...@@ -65,6 +88,7 @@ module API ...@@ -65,6 +88,7 @@ module API
path = Gitlab::Ci::Build::Artifacts::Path path = Gitlab::Ci::Build::Artifacts::Path
.new(params[:artifact_path]) .new(params[:artifact_path])
bad_request! unless path.valid? bad_request! unless path.valid?
send_artifacts_entry(build, path) send_artifacts_entry(build, path)
......
...@@ -1897,7 +1897,7 @@ describe Project do ...@@ -1897,7 +1897,7 @@ describe Project do
end end
end end
describe '#latest_successful_builds_for' do describe '#latest_successful_builds_for and #latest_successful_build_for' do
def create_pipeline(status = 'success') def create_pipeline(status = 'success')
create(:ci_pipeline, project: project, create(:ci_pipeline, project: project,
sha: project.commit.sha, sha: project.commit.sha,
...@@ -1919,14 +1919,16 @@ describe Project do ...@@ -1919,14 +1919,16 @@ describe Project do
it 'gives the latest builds from latest pipeline' do it 'gives the latest builds from latest pipeline' do
pipeline1 = create_pipeline pipeline1 = create_pipeline
pipeline2 = create_pipeline pipeline2 = create_pipeline
build1_p2 = create_build(pipeline2, 'test')
create_build(pipeline1, 'test') create_build(pipeline1, 'test')
create_build(pipeline1, 'test2') create_build(pipeline1, 'test2')
build1_p2 = create_build(pipeline2, 'test')
build2_p2 = create_build(pipeline2, 'test2') build2_p2 = create_build(pipeline2, 'test2')
latest_builds = project.latest_successful_builds_for latest_builds = project.latest_successful_builds_for
single_build = project.latest_successful_build_for(build1_p2.name)
expect(latest_builds).to contain_exactly(build2_p2, build1_p2) expect(latest_builds).to contain_exactly(build2_p2, build1_p2)
expect(single_build).to eq(build1_p2)
end end
end end
...@@ -1936,16 +1938,22 @@ describe Project do ...@@ -1936,16 +1938,22 @@ describe Project do
context 'standalone pipeline' do context 'standalone pipeline' do
it 'returns builds for ref for default_branch' do it 'returns builds for ref for default_branch' do
builds = project.latest_successful_builds_for builds = project.latest_successful_builds_for
single_build = project.latest_successful_build_for(build.name)
expect(builds).to contain_exactly(build) expect(builds).to contain_exactly(build)
expect(single_build).to eq(build)
end end
it 'returns empty relation if the build cannot be found' do it 'returns empty relation if the build cannot be found for #latest_successful_builds_for' do
builds = project.latest_successful_builds_for('TAIL') builds = project.latest_successful_builds_for('TAIL')
expect(builds).to be_kind_of(ActiveRecord::Relation) expect(builds).to be_kind_of(ActiveRecord::Relation)
expect(builds).to be_empty expect(builds).to be_empty
end end
it 'returns exception if the build cannot be found for #latest_successful_build_for' do
expect { project.latest_successful_build_for(build.name, 'TAIL') }.to raise_error(ActiveRecord::RecordNotFound)
end
end end
context 'with some pending pipeline' do context 'with some pending pipeline' do
...@@ -1954,9 +1962,11 @@ describe Project do ...@@ -1954,9 +1962,11 @@ describe Project do
end end
it 'gives the latest build from latest pipeline' do it 'gives the latest build from latest pipeline' do
latest_build = project.latest_successful_builds_for latest_builds = project.latest_successful_builds_for
last_single_build = project.latest_successful_build_for(build.name)
expect(latest_build).to contain_exactly(build) expect(latest_builds).to contain_exactly(build)
expect(last_single_build).to eq(build)
end end
end end
end end
......
...@@ -586,6 +586,136 @@ describe API::Jobs do ...@@ -586,6 +586,136 @@ describe API::Jobs do
end end
end end
describe 'GET id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name' do
context 'when job has artifacts' do
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) }
let(:artifact) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' }
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
let(:public_builds) { true }
before do
stub_artifacts_object_storage
job.success
project.update(visibility_level: visibility_level,
public_builds: public_builds)
get_artifact_file(artifact)
end
context 'when user is anonymous' do
let(:api_user) { nil }
context 'when project is public' do
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
let(:public_builds) { true }
it 'allows to access artifacts' do
expect(response).to have_gitlab_http_status(200)
expect(response.headers.to_h)
.to include('Content-Type' => 'application/json',
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
end
end
context 'when project is public with builds access disabled' do
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
let(:public_builds) { false }
it 'rejects access to artifacts' do
expect(response).to have_gitlab_http_status(403)
expect(json_response).to have_key('message')
expect(response.headers.to_h)
.not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
end
end
context 'when project is private' do
let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
let(:public_builds) { true }
it 'rejects access and hides existence of artifacts' do
expect(response).to have_gitlab_http_status(404)
expect(json_response).to have_key('message')
expect(response.headers.to_h)
.not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
end
end
end
context 'when user is authorized' do
let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
let(:public_builds) { true }
it 'returns a specific artifact file for a valid path' do
expect(Gitlab::Workhorse)
.to receive(:send_artifacts_entry)
.and_call_original
get_artifact_file(artifact)
expect(response).to have_gitlab_http_status(200)
expect(response.headers.to_h)
.to include('Content-Type' => 'application/json',
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
end
end
context 'with branch name containing slash' do
before do
pipeline.reload
pipeline.update(ref: 'improve/awesome',
sha: project.commit('improve/awesome').sha)
end
it 'returns a specific artifact file for a valid path' do
get_artifact_file(artifact, 'improve/awesome')
expect(response).to have_gitlab_http_status(200)
expect(response.headers.to_h)
.to include('Content-Type' => 'application/json',
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
end
end
context 'non-existing job' do
shared_examples 'not found' do
it { expect(response).to have_gitlab_http_status(:not_found) }
end
context 'has no such ref' do
before do
get_artifact_file('some/artifact', 'wrong-ref')
end
it_behaves_like 'not found'
end
context 'has no such job' do
before do
get_artifact_file('some/artifact', pipeline.ref, 'wrong-job-name')
end
it_behaves_like 'not found'
end
end
end
context 'when job does not have artifacts' do
let(:job) { create(:ci_build, pipeline: pipeline, user: api_user) }
it 'does not return job artifact file' do
get_artifact_file('some/artifact')
expect(response).to have_gitlab_http_status(404)
end
end
def get_artifact_file(artifact_path, ref = pipeline.ref, job_name = job.name)
get api("/projects/#{project.id}/jobs/artifacts/#{ref}/raw/#{artifact_path}", api_user), job: job_name
end
end
describe 'GET /projects/:id/jobs/:job_id/trace' do describe 'GET /projects/:id/jobs/:job_id/trace' do
before do before do
get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user) get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user)
......
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