diff options
author | Steve Azzopardi <steveazz@outlook.com> | 2018-12-04 11:50:01 +0100 |
---|---|---|
committer | Steve Azzopardi <steveazz@outlook.com> | 2018-12-07 15:33:30 +0100 |
commit | 401f65c43aa12aa712daa8ddfb00a4fb731541c8 (patch) | |
tree | a47285f34754b7339225a1baf9db2caa0ea7b7bf | |
parent | 62d971129da99936a3cdc04f3740d26f16a0c7a6 (diff) | |
download | gitlab-ce-401f65c43aa12aa712daa8ddfb00a4fb731541c8.tar.gz |
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
-rw-r--r-- | app/models/project.rb | 5 | ||||
-rw-r--r-- | changelogs/unreleased/54626-able-to-download-a-single-archive-file-with-api-by-ref-name.yml | 5 | ||||
-rw-r--r-- | doc/api/jobs.md | 37 | ||||
-rw-r--r-- | lib/api/job_artifacts.rb | 24 | ||||
-rw-r--r-- | spec/models/project_spec.rb | 20 | ||||
-rw-r--r-- | spec/requests/api/jobs_spec.rb | 130 |
6 files changed, 215 insertions, 6 deletions
diff --git a/app/models/project.rb b/app/models/project.rb index 587bada469e..6a8d8880e39 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -649,6 +649,11 @@ class Project < ActiveRecord::Base 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) sha = repository.merge_base(first_commit_id, second_commit_id) commit_by(oid: sha) if sha diff --git a/changelogs/unreleased/54626-able-to-download-a-single-archive-file-with-api-by-ref-name.yml b/changelogs/unreleased/54626-able-to-download-a-single-archive-file-with-api-by-ref-name.yml new file mode 100644 index 00000000000..fa905b47ca2 --- /dev/null +++ b/changelogs/unreleased/54626-able-to-download-a-single-archive-file-with-api-by-ref-name.yml @@ -0,0 +1,5 @@ +--- +title: Add new endpoint to download single artifact file for a ref +merge_request: 23538 +author: +type: added diff --git a/doc/api/jobs.md b/doc/api/jobs.md index aa290ff4cf8..589c48ee08d 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -404,7 +404,7 @@ Example response: [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 @@ -438,6 +438,41 @@ Example response: | 400 | Invalid path provided | | 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 of a specific job of a project diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb index 7c2d8ff11bf..a4068a200b3 100644 --- a/lib/api/job_artifacts.rb +++ b/lib/api/job_artifacts.rb @@ -35,6 +35,29 @@ module API end # 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 detail 'This feature was introduced in GitLab 8.5' end @@ -65,6 +88,7 @@ module API path = Gitlab::Ci::Build::Artifacts::Path .new(params[:artifact_path]) + bad_request! unless path.valid? send_artifacts_entry(build, path) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 50920d9d1fc..2497f9c624d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1897,7 +1897,7 @@ describe Project do end end - describe '#latest_successful_builds_for' do + describe '#latest_successful_builds_for and #latest_successful_build_for' do def create_pipeline(status = 'success') create(:ci_pipeline, project: project, sha: project.commit.sha, @@ -1919,14 +1919,16 @@ describe Project do it 'gives the latest builds from latest pipeline' do pipeline1 = create_pipeline pipeline2 = create_pipeline - build1_p2 = create_build(pipeline2, 'test') create_build(pipeline1, 'test') create_build(pipeline1, 'test2') + build1_p2 = create_build(pipeline2, 'test') build2_p2 = create_build(pipeline2, 'test2') 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(single_build).to eq(build1_p2) end end @@ -1936,16 +1938,22 @@ describe Project do context 'standalone pipeline' do it 'returns builds for ref for default_branch' do builds = project.latest_successful_builds_for + single_build = project.latest_successful_build_for(build.name) expect(builds).to contain_exactly(build) + expect(single_build).to eq(build) 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') expect(builds).to be_kind_of(ActiveRecord::Relation) expect(builds).to be_empty 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 context 'with some pending pipeline' do @@ -1954,9 +1962,11 @@ describe Project do end 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 diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index 8770365c893..cd4e480ca64 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -586,6 +586,136 @@ describe API::Jobs do 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 before do get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user) |