Commit c30d37a8 authored by Vijay Hawoldar's avatar Vijay Hawoldar

Add raw snippet endpoints to support files

With the addition of multiple file support, this change
allows API requests to specify a `ref` and `file_path` in
order to get the RAW content for a specific file at a specific
commit
parent 0382457b
---
title: Add raw snippet repository file endpoint to API
merge_request: 36037
author:
type: changed
......@@ -183,6 +183,28 @@ curl "https://gitlab.com/api/v4/projects/:id/snippets/:snippet_id/raw" \
--header "PRIVATE-TOKEN: <your_access_token>"
```
## Snippet repository file content
Returns the raw file content as plain text.
```plaintext
GET /projects/:id/snippets/:snippet_id/files/:ref/:file_path/raw
```
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `snippet_id` (required) - The ID of a project's snippet
- `ref` (required) - The name of a branch, tag or commit e.g. master
- `file_path` (required) - The URL-encoded path to the file, e.g. snippet%2Erb
Example request:
```shell
curl "https://gitlab.com/api/v4/projects/1/snippets/2/files/master/snippet%2Erb/raw" \
--header "PRIVATE-TOKEN: <your_access_token>"
```
## Get user agent details
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/29508) in GitLab 9.4.
......
......@@ -150,6 +150,34 @@ Example response:
Hello World snippet
```
## Snippet repository file content
Returns the raw file content as plain text.
```plaintext
GET /snippets/:id/files/:ref/:file_path/raw
```
Parameters:
| Attribute | Type | Required | Description |
|:------------|:--------|:---------|:-------------------------------------------------------------------|
| `id` | integer | yes | ID of snippet to retrieve |
| `ref` | string | yes | Reference to a tag, branch or commit |
| `file_path` | string | yes | URL-encoded path to the file |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/snippets/1/files/master/snippet%2Erb/raw"
```
Example response:
```plaintext
Hello World snippet
```
## Create new snippet
Create a new snippet.
......
......@@ -3,15 +3,37 @@
module API
module Helpers
module SnippetsHelpers
extend Grape::API::Helpers
params :raw_file_params do
requires :file_path, type: String, file_path: true, desc: 'The url encoded path to the file, e.g. lib%2Fclass%2Erb'
requires :ref, type: String, desc: 'The name of branch, tag or commit'
end
def content_for(snippet)
if snippet.empty_repo?
env['api.format'] = :txt
content_type 'text/plain'
header['Content-Disposition'] = 'attachment'
snippet.content
else
blob = snippet.blobs.first
blob.load_all_data!
blob.data
send_git_blob(blob.repository, blob)
end
end
def file_content_for(snippet)
repo = snippet.repository
commit = repo.commit(params[:ref])
not_found!('Reference') unless commit
blob = repo.blob_at(commit.sha, params[:file_path])
not_found!('File') unless blob
send_git_blob(repo, blob)
end
end
end
end
......@@ -147,10 +147,19 @@ module API
snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
not_found!('Snippet') unless snippet
env['api.format'] = :txt
content_type 'text/plain'
present content_for(snippet)
end
desc 'Get raw project snippet file contents from the repository'
params do
use :raw_file_params
end
get ":id/snippets/:snippet_id/files/:ref/:file_path/raw", requirements: { file_path: API::NO_SLASH_URL_PART_REGEX } do
snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
not_found!('Snippet') unless snippet&.repo_exists?
present file_content_for(snippet)
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Get the user agent details for a project snippet' do
......
......@@ -155,14 +155,22 @@ module API
end
get ":id/raw" do
snippet = snippets.find_by_id(params.delete(:id))
break not_found!('Snippet') unless snippet
not_found!('Snippet') unless snippet
env['api.format'] = :txt
content_type 'text/plain'
header['Content-Disposition'] = 'attachment'
present content_for(snippet)
end
desc 'Get raw snippet file contents from the repository'
params do
use :raw_file_params
end
get ":id/files/:ref/:file_path/raw", requirements: { file_path: API::NO_SLASH_URL_PART_REGEX } do
snippet = snippets.find_by_id(params.delete(:id))
not_found!('Snippet') unless snippet&.repo_exists?
present file_content_for(snippet)
end
desc 'Get the user agent details for a snippet' do
success Entities::UserAgentDetail
end
......
......@@ -474,4 +474,12 @@ RSpec.describe API::ProjectSnippets do
subject { get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", snippet.author) }
end
end
describe 'GET /projects/:project_id/snippets/:id/files/:ref/:file_path/raw' do
let_it_be(:snippet) { create(:project_snippet, :repository, author: admin, project: project) }
it_behaves_like 'raw snippet files' do
let(:api_path) { "/projects/#{snippet.project.id}/snippets/#{snippet_id}/files/#{ref}/#{file_path}/raw" }
end
end
end
......@@ -107,12 +107,7 @@ RSpec.describe API::Snippets do
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq 'text/plain'
end
it 'forces attachment content disposition' do
get api("/snippets/#{snippet.id}/raw", author)
expect(headers['Content-Disposition']).to match(/^attachment/)
expect(headers['Content-Disposition']).to match(/^inline/)
end
it 'returns 404 for invalid snippet id' do
......@@ -145,6 +140,14 @@ RSpec.describe API::Snippets do
end
end
describe 'GET /snippets/:id/files/:ref/:file_path/raw' do
let_it_be(:snippet) { create(:personal_snippet, :repository, :private) }
it_behaves_like 'raw snippet files' do
let(:api_path) { "/snippets/#{snippet_id}/files/#{ref}/#{file_path}/raw" }
end
end
describe 'GET /snippets/:id' do
let_it_be(:admin) { create(:user, :admin) }
let_it_be(:author) { create(:user) }
......
# frozen_string_literal: true
RSpec.shared_examples 'raw snippet files' do
let_it_be(:unauthorized_user) { create(:user) }
let(:snippet_id) { snippet.id }
let(:user) { snippet.author }
let(:file_path) { '%2Egitattributes' }
let(:ref) { 'master' }
context 'with no user' do
it 'requires authentication' do
get api(api_path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
shared_examples 'not found' do
it 'returns 404' do
get api(api_path, user)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end
context 'when not authorized' do
let(:user) { unauthorized_user }
it_behaves_like 'not found'
end
context 'with an invalid snippet ID' do
let(:snippet_id) { 'invalid' }
it_behaves_like 'not found'
end
context 'with valid params' do
it 'returns the raw file info' do
expect(Gitlab::Workhorse).to receive(:send_git_blob).and_call_original
get api(api_path, user)
aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq 'text/plain'
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true'
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
expect(response.header['Content-Disposition']).to match 'filename=".gitattributes"'
end
end
end
context 'with invalid params' do
using RSpec::Parameterized::TableSyntax
where(:file_path, :ref, :status, :key, :message) do
'%2Egitattributes' | 'invalid-ref' | :not_found | 'message' | '404 Reference Not Found'
'%2Egitattributes' | nil | :not_found | 'error' | '404 Not Found'
'%2Egitattributes' | '' | :not_found | 'error' | '404 Not Found'
'doesnotexist.rb' | 'master' | :not_found | 'message' | '404 File Not Found'
'/does/not/exist.rb' | 'master' | :not_found | 'error' | '404 Not Found'
'%2E%2E%2Fetc%2Fpasswd' | 'master' | :bad_request | 'error' | 'file_path should be a valid file path'
'%2Fetc%2Fpasswd' | 'master' | :bad_request | 'error' | 'file_path should be a valid file path'
'../../etc/passwd' | 'master' | :not_found | 'error' | '404 Not Found'
end
with_them do
before do
get api(api_path, user)
end
it { expect(response).to have_gitlab_http_status(status) }
it { expect(json_response[key]).to eq(message) }
end
end
end
......@@ -76,9 +76,12 @@ end
RSpec.shared_examples 'snippet blob content' do
it 'returns content from repository' do
expect(Gitlab::Workhorse).to receive(:send_git_blob).and_call_original
subject
expect(response.body).to eq(snippet.blobs.first.data)
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true'
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
context 'when snippet repository is empty' do
......
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