From 2d43ab11d4f437ac8cd279842400ee4e8c417fb6 Mon Sep 17 00:00:00 2001 From: bmarjanovic Date: Wed, 7 Feb 2024 14:34:23 +0100 Subject: [PATCH 1/2] Adds download API v4 endpoint for project avatar Changelog: added --- doc/api/projects.md | 21 +++++ lib/api/api.rb | 1 + lib/api/project_avatar.rb | 33 +++++++ spec/requests/api/project_avatar_spec.rb | 106 +++++++++++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 lib/api/project_avatar.rb create mode 100644 spec/requests/api/project_avatar_spec.rb diff --git a/doc/api/projects.md b/doc/api/projects.md index b33b5dec3cd7b2..3b0bf11e84e5d8 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -2639,6 +2639,27 @@ Returned object: } ``` +## Download a Project avatar + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144039) in GitLab 16.9. + +Get a project avatar. This endpoint can be accessed without authentication if the +project is publicly accessible. + +```plaintext +GET /projects/:id/avatar +``` + +| Attribute | Type | Required | Description | +| --------- | -------------- | -------- | --------------------- | +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). | + +Example: + +```shell +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/4/avatar" +``` + ## Remove a project avatar > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92604) in GitLab 15.4. diff --git a/lib/api/api.rb b/lib/api/api.rb index 00d7425cc75050..9982051d6954bb 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -297,6 +297,7 @@ def initialize(location_url) mount ::API::PagesDomains mount ::API::PersonalAccessTokens::SelfInformation mount ::API::PersonalAccessTokens + mount ::API::ProjectAvatar mount ::API::ProjectClusters mount ::API::ProjectContainerRepositories mount ::API::ProjectDebianDistributions diff --git a/lib/api/project_avatar.rb b/lib/api/project_avatar.rb new file mode 100644 index 00000000000000..28a51800bc4f89 --- /dev/null +++ b/lib/api/project_avatar.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module API + class ProjectAvatar < ::API::Base + feature_category :groups_and_projects + + params do + requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Download the project avatar' do + detail 'This feature was introduced in GitLab 16.9' + tags %w[project_avatar] + success code: 200 + end + get ':id/avatar' do + avatar = user_project.avatar + + not_found!('Avatar') if avatar.blank? + + header( + 'Content-Disposition', + ActionDispatch::Http::ContentDisposition.format( + disposition: 'attachment', + filename: avatar.filename + ) + ) + + present_carrierwave_file!(avatar) + end + end + end +end diff --git a/spec/requests/api/project_avatar_spec.rb b/spec/requests/api/project_avatar_spec.rb new file mode 100644 index 00000000000000..47d2126dc5e7c9 --- /dev/null +++ b/spec/requests/api/project_avatar_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::ProjectAvatar, feature_category: :groups_and_projects do + def avatar_path(project) + "/projects/#{ERB::Util.url_encode(project.full_path)}/avatar" + end + + describe 'GET /projects/:id/avatar' do + context 'when the project is public' do + let(:project) { create(:project, :public, :with_avatar) } + + it 'retrieves the avatar successfully' do + get api(avatar_path(project)) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Content-Disposition']) + .to eq(%(attachment; filename="dk.png"; filename*=UTF-8''dk.png)) + end + + context 'when the avatar is in the object storage' do + before do + stub_uploads_object_storage(AvatarUploader) + + project.avatar.migrate!(ObjectStorage::Store::REMOTE) + end + + it 'redirects to the file in the object storage' do + get api(avatar_path(project)) + + expect(response).to have_gitlab_http_status(:found) + expect(response.headers['Content-Disposition']) + .to eq(%(attachment; filename="dk.png"; filename*=UTF-8''dk.png)) + end + end + + context 'when the project does not have avatar' do + let(:project) { create(:project, :public) } + + it 'returns :not_found' do + get api(avatar_path(project)) + + expect(response).to have_gitlab_http_status(:not_found) + expect(response.body).to eq(%({"message":"404 Avatar Not Found"})) + end + end + + context 'when the project is in a group' do + let(:project) { create(:project, :in_group, :public, :with_avatar) } + + it 'returns :ok' do + get api(avatar_path(project)) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when the project is in a subgroup' do + let(:project) { create(:project, :in_subgroup, :public, :with_avatar) } + + it 'returns :ok' do + get api(avatar_path(project)) + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'when the project is private' do + let(:project) { create(:project, :private, :with_avatar) } + + context 'when the user is not authenticated' do + it 'returns :not_found' do + get api(avatar_path(project)) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the the project user is authenticated' do + context 'and have access to the project' do + let(:owner) { create(:user) } + + before do + project.add_owner(owner) + end + + it 'retrieves the avatar successfully' do + get api(avatar_path(project), owner) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'and does not have access to the project' do + it 'returns :not_found' do + get api(avatar_path(project), create(:user)) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + end +end -- GitLab From ab00106504cf73d01333c1db824ee7a4d30af439 Mon Sep 17 00:00:00 2001 From: Ashraf Khamis Date: Wed, 7 Feb 2024 14:59:00 +0000 Subject: [PATCH 2/2] Apply TW suggestions --- doc/api/projects.md | 12 ++++++------ lib/api/project_avatar.rb | 4 ++-- spec/requests/api/project_avatar_spec.rb | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/api/projects.md b/doc/api/projects.md index 3b0bf11e84e5d8..f43f349764fdeb 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -2639,20 +2639,20 @@ Returned object: } ``` -## Download a Project avatar +## Download a project avatar > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144039) in GitLab 16.9. -Get a project avatar. This endpoint can be accessed without authentication if the -project is publicly accessible. +Get a project avatar. +You can access this endpoint without authentication if the project is publicly accessible. ```plaintext GET /projects/:id/avatar ``` -| Attribute | Type | Required | Description | -| --------- | -------------- | -------- | --------------------- | -| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). | +| Attribute | Type | Required | Description | +| --------- | ----------------- | -------- | --------------------- | +| `id` | integer or string | yes | ID or [URL-encoded path](rest/index.md#namespaced-path-encoding) of the project. | Example: diff --git a/lib/api/project_avatar.rb b/lib/api/project_avatar.rb index 28a51800bc4f89..e674169bba70d7 100644 --- a/lib/api/project_avatar.rb +++ b/lib/api/project_avatar.rb @@ -5,10 +5,10 @@ class ProjectAvatar < ::API::Base feature_category :groups_and_projects params do - requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' + requires :id, types: [String, Integer], desc: 'ID or URL-encoded path of the project' end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc 'Download the project avatar' do + desc 'Download a project avatar' do detail 'This feature was introduced in GitLab 16.9' tags %w[project_avatar] success code: 200 diff --git a/spec/requests/api/project_avatar_spec.rb b/spec/requests/api/project_avatar_spec.rb index 47d2126dc5e7c9..e677a3728c90eb 100644 --- a/spec/requests/api/project_avatar_spec.rb +++ b/spec/requests/api/project_avatar_spec.rb @@ -19,14 +19,14 @@ def avatar_path(project) .to eq(%(attachment; filename="dk.png"; filename*=UTF-8''dk.png)) end - context 'when the avatar is in the object storage' do + context 'when the avatar is in object storage' do before do stub_uploads_object_storage(AvatarUploader) project.avatar.migrate!(ObjectStorage::Store::REMOTE) end - it 'redirects to the file in the object storage' do + it 'redirects to the file in object storage' do get api(avatar_path(project)) expect(response).to have_gitlab_http_status(:found) @@ -35,7 +35,7 @@ def avatar_path(project) end end - context 'when the project does not have avatar' do + context 'when the project does not have an avatar' do let(:project) { create(:project, :public) } it 'returns :not_found' do @@ -78,7 +78,7 @@ def avatar_path(project) end end - context 'when the the project user is authenticated' do + context 'when the project user is authenticated' do context 'and have access to the project' do let(:owner) { create(:user) } -- GitLab