Commit 470c68c9 authored by Ash McKenzie's avatar Ash McKenzie

Merge branch 'ajk-group-wiki-attachments' into 'master'

[Group Wiki] Update attachment API to support Group Level Wiki

Closes #212199

See merge request gitlab-org/gitlab!34232
parents ce38822c 2a742324
# Wikis API
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/212199) in GitLab 13.2.
Available only in APIv4.
## List wiki pages
List all wiki pages for a given group.
```plaintext
GET /groups/:id/wikis
```
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `with_content` | boolean | no | Include pages' content |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/1/wikis?with_content=1"
```
Example response:
```json
[
{
"content" : "Here is an instruction how to deploy this project.",
"format" : "markdown",
"slug" : "deploy",
"title" : "deploy"
},
{
"content" : "Our development process is described here.",
"format" : "markdown",
"slug" : "development",
"title" : "development"
},{
"content" : "* [Deploy](deploy)\n* [Development](development)",
"format" : "markdown",
"slug" : "home",
"title" : "home"
}
]
```
## Get a wiki page
Get a wiki page for a given group.
```plaintext
GET /groups/:id/wikis/:slug
```
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `slug` | string | yes | The slug (a unique string) of the wiki page |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/1/wikis/home"
```
Example response:
```json
{
"content" : "home page",
"format" : "markdown",
"slug" : "home",
"title" : "home"
}
```
## Create a new wiki page
Create a new wiki page for the given repository with the given title, slug, and content.
```plaintext
POST /projects/:id/wikis
```
| Attribute | Type | Required | Description |
| ------------- | ------- | -------- | ---------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `content` | string | yes | The content of the wiki page |
| `title` | string | yes | The title of the wiki page |
| `format` | string | no | The format of the wiki page. Available formats are: `markdown` (default), `rdoc`, `asciidoc` and `org` |
```shell
curl --data "format=rdoc&title=Hello&content=Hello world" \
--header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/groups/1/wikis"
```
Example response:
```json
{
"content" : "Hello world",
"format" : "markdown",
"slug" : "Hello",
"title" : "Hello"
}
```
## Edit an existing wiki page
Update an existing wiki page. At least one parameter is required to update the wiki page.
```plaintext
PUT /groups/:id/wikis/:slug
```
| Attribute | Type | Required | Description |
| --------------- | ------- | --------------------------------- | ------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `content` | string | yes if `title` is not provided | The content of the wiki page |
| `title` | string | yes if `content` is not provided | The title of the wiki page |
| `format` | string | no | The format of the wiki page. Available formats are: `markdown` (default), `rdoc`, `asciidoc` and `org` |
| `slug` | string | yes | The slug (a unique identifier) of the wiki page |
```shell
curl --request PUT --data "format=rdoc&content=documentation&title=Docs" \
--header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/groups/1/wikis/foo"
```
Example response:
```json
{
"content" : "documentation",
"format" : "markdown",
"slug" : "Docs",
"title" : "Docs"
}
```
## Delete a wiki page
Delete a wiki page with a given slug.
```plaintext
DELETE /groups/:id/wikis/:slug
```
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `slug` | string | yes | The slug (a unique identifier) of the wiki page |
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/1/wikis/foo"
```
On success the HTTP status code is `204` and no JSON response is expected.
## Upload an attachment to the wiki repository
Upload a file to the attachment folder inside the wiki's repository. The
attachment folder is the `uploads` folder.
```plaintext
POST /groups/:id/wikis/attachments
```
| Attribute | Type | Required | Description |
| ------------- | ------- | -------- | ---------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `file` | string | yes | The attachment to be uploaded |
| `branch` | string | no | The name of the branch. Defaults to the wiki repository default branch |
To upload a file from your filesystem, use the `--form` argument. This causes
cURL to post data using the header `Content-Type: multipart/form-data`.
The `file=` parameter must point to a file on your filesystem and be preceded
by `@`. For example:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "file=@dk.png" "https://gitlab.example.com/api/v4/groups/1/wikis/attachments"
```
Example response:
```json
{
"file_name" : "dk.png",
"file_path" : "uploads/6a061c4cf9f1c28cb22c384b4b8d4e3c/dk.png",
"branch" : "master",
"link" : {
"url" : "uploads/6a061c4cf9f1c28cb22c384b4b8d4e3c/dk.png",
"markdown" : "![dk](uploads/6a061c4cf9f1c28cb22c384b4b8d4e3c/dk.png)"
}
}
```
---
title: Add group wiki REST API
merge_request: 34232
author:
type: added
# frozen_string_literal: true
module EE
module API
module Helpers
module WikisHelpers
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
class_methods do
def wiki_resource_kinds
[:groups, *super]
end
end
override :find_container
def find_container(kind)
return user_group if kind == :groups
super
end
end
end
end
end
This diff is collapsed.
...@@ -79,12 +79,6 @@ module API ...@@ -79,12 +79,6 @@ module API
@project ||= find_project!(params[:id]) @project ||= find_project!(params[:id])
end end
def wiki_page
page = ProjectWiki.new(user_project, current_user).find_page(params[:slug])
page || not_found!('Wiki Page')
end
def available_labels_for(label_parent, include_ancestor_groups: true) def available_labels_for(label_parent, include_ancestor_groups: true)
search_params = { include_ancestor_groups: include_ancestor_groups } search_params = { include_ancestor_groups: include_ancestor_groups }
......
# frozen_string_literal: true
module API
module Helpers
module WikisHelpers
def self.wiki_resource_kinds
[:projects]
end
def find_container(kind)
return user_project if kind == :projects
raise "Unknown wiki container #{kind}"
end
def wiki_page
Wiki.for_container(container, current_user).find_page(params[:slug]) || not_found!('Wiki Page')
end
def commit_params(attrs)
base_params = { branch_name: attrs[:branch] }
file_details = case attrs[:file]
when Hash # legacy format: TODO remove when we drop support for non accelerated uploads
{ file_name: attrs[:file][:filename], file_content: attrs[:file][:tempfile].read }
else
{ file_name: attrs[:file].original_filename, file_content: attrs[:file].read }
end
base_params.merge(file_details)
end
end
end
end
API::Helpers::WikisHelpers.prepend_if_ee('EE::API::Helpers::WikisHelpers')
...@@ -2,24 +2,10 @@ ...@@ -2,24 +2,10 @@
module API module API
class Wikis < Grape::API class Wikis < Grape::API
helpers ::API::Helpers::WikisHelpers
helpers do helpers do
def commit_params(attrs) attr_reader :container
# In order to avoid service disruption this can work with an old workhorse without the acceleration
# the first branch of this if must be removed when we drop support for non accelerated uploads
if attrs[:file].is_a?(Hash)
{
file_name: attrs[:file][:filename],
file_content: attrs[:file][:tempfile].read,
branch_name: attrs[:branch]
}
else
{
file_name: attrs[:file].original_filename,
file_content: attrs[:file].read,
branch_name: attrs[:branch]
}
end
end
params :common_wiki_page_params do params :common_wiki_page_params do
optional :format, optional :format,
...@@ -32,7 +18,12 @@ module API ...@@ -32,7 +18,12 @@ module API
WIKI_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(slug: API::NO_SLASH_URL_PART_REGEX) WIKI_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(slug: API::NO_SLASH_URL_PART_REGEX)
resource :projects, requirements: WIKI_ENDPOINT_REQUIREMENTS do ::API::Helpers::WikisHelpers.wiki_resource_kinds.each do |container_resource|
resource container_resource, requirements: WIKI_ENDPOINT_REQUIREMENTS do
after_validation do
@container = Gitlab::Lazy.new { find_container(container_resource) }
end
desc 'Get a list of wiki pages' do desc 'Get a list of wiki pages' do
success Entities::WikiPageBasic success Entities::WikiPageBasic
end end
...@@ -40,11 +31,11 @@ module API ...@@ -40,11 +31,11 @@ module API
optional :with_content, type: Boolean, default: false, desc: "Include pages' content" optional :with_content, type: Boolean, default: false, desc: "Include pages' content"
end end
get ':id/wikis' do get ':id/wikis' do
authorize! :read_wiki, user_project authorize! :read_wiki, container
entity = params[:with_content] ? Entities::WikiPage : Entities::WikiPageBasic entity = params[:with_content] ? Entities::WikiPage : Entities::WikiPageBasic
present user_project.wiki.list_pages(load_content: params[:with_content]), with: entity present container.wiki.list_pages(load_content: params[:with_content]), with: entity
end end
desc 'Get a wiki page' do desc 'Get a wiki page' do
...@@ -54,7 +45,7 @@ module API ...@@ -54,7 +45,7 @@ module API
requires :slug, type: String, desc: 'The slug of a wiki page' requires :slug, type: String, desc: 'The slug of a wiki page'
end end
get ':id/wikis/:slug' do get ':id/wikis/:slug' do
authorize! :read_wiki, user_project authorize! :read_wiki, container
present wiki_page, with: Entities::WikiPage present wiki_page, with: Entities::WikiPage
end end
...@@ -68,9 +59,9 @@ module API ...@@ -68,9 +59,9 @@ module API
use :common_wiki_page_params use :common_wiki_page_params
end end
post ':id/wikis' do post ':id/wikis' do
authorize! :create_wiki, user_project authorize! :create_wiki, container
page = WikiPages::CreateService.new(container: user_project, current_user: current_user, params: params).execute page = WikiPages::CreateService.new(container: container, current_user: current_user, params: params).execute
if page.valid? if page.valid?
present page, with: Entities::WikiPage present page, with: Entities::WikiPage
...@@ -89,9 +80,11 @@ module API ...@@ -89,9 +80,11 @@ module API
at_least_one_of :content, :title, :format at_least_one_of :content, :title, :format
end end
put ':id/wikis/:slug' do put ':id/wikis/:slug' do
authorize! :create_wiki, user_project authorize! :create_wiki, container
page = WikiPages::UpdateService.new(container: user_project, current_user: current_user, params: params).execute(wiki_page) page = WikiPages::UpdateService
.new(container: container, current_user: current_user, params: params)
.execute(wiki_page)
if page.valid? if page.valid?
present page, with: Entities::WikiPage present page, with: Entities::WikiPage
...@@ -105,9 +98,11 @@ module API ...@@ -105,9 +98,11 @@ module API
requires :slug, type: String, desc: 'The slug of a wiki page' requires :slug, type: String, desc: 'The slug of a wiki page'
end end
delete ':id/wikis/:slug' do delete ':id/wikis/:slug' do
authorize! :admin_wiki, user_project authorize! :admin_wiki, container
WikiPages::DestroyService.new(container: user_project, current_user: current_user).execute(wiki_page) WikiPages::DestroyService
.new(container: container, current_user: current_user)
.execute(wiki_page)
no_content! no_content!
end end
...@@ -121,10 +116,10 @@ module API ...@@ -121,10 +116,10 @@ module API
optional :branch, type: String, desc: 'The name of the branch' optional :branch, type: String, desc: 'The name of the branch'
end end
post ":id/wikis/attachments" do post ":id/wikis/attachments" do
authorize! :create_wiki, user_project authorize! :create_wiki, container
result = ::Wikis::CreateAttachmentService.new( result = ::Wikis::CreateAttachmentService.new(
container: user_project, container: container,
current_user: current_user, current_user: current_user,
params: commit_params(declared_params(include_missing: false)) params: commit_params(declared_params(include_missing: false))
).execute ).execute
...@@ -138,4 +133,5 @@ module API ...@@ -138,4 +133,5 @@ module API
end end
end end
end end
end
end end
This diff is collapsed.
# frozen_string_literal: true
RSpec.shared_examples_for 'wikis API returns list of wiki pages' do
context 'when wiki has pages' do
let!(:pages) do
[create(:wiki_page, wiki: wiki, title: 'page1', content: 'content of page1'),
create(:wiki_page, wiki: wiki, title: 'page2.with.dot', content: 'content of page2')]
end
it 'returns the list of wiki pages without content' do
get api(url, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(2)
json_response.each_with_index do |page, index|
expect(page.keys).to match_array(expected_keys_without_content)
expect(page['slug']).to eq(pages[index].slug)
expect(page['title']).to eq(pages[index].title)
end
end
it 'returns the list of wiki pages with content' do
get api(url, user), params: { with_content: 1 }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(2)
json_response.each_with_index do |page, index|
expect(page.keys).to match_array(expected_keys_with_content)
expect(page['content']).to eq(pages[index].content)
expect(page['slug']).to eq(pages[index].slug)
expect(page['title']).to eq(pages[index].title)
end
end
end
it 'return the empty list of wiki pages' do
get api(url, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(0)
end
end
RSpec.shared_examples_for 'wikis API returns wiki page' do
it 'returns the wiki page' do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(4)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(page.content)
expect(json_response['slug']).to eq(page.slug)
expect(json_response['title']).to eq(page.title)
end
end
RSpec.shared_examples_for 'wikis API creates wiki page' do
it 'creates the wiki page' do
post(api(url, user), params: payload)
expect(response).to have_gitlab_http_status(:created)
expect(json_response.size).to eq(4)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(payload[:content])
expect(json_response['slug']).to eq(payload[:title].tr(' ', '-'))
expect(json_response['title']).to eq(payload[:title])
expect(json_response['rdoc']).to eq(payload[:rdoc])
end
[:title, :content].each do |part|
it "responds with validation error on empty #{part}" do
payload.delete(part)
post(api(url, user), params: payload)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response.size).to eq(1)
expect(json_response['error']).to eq("#{part} is missing")
end
end
end
RSpec.shared_examples_for 'wikis API updates wiki page' do
it 'updates the wiki page' do
put(api(url, user), params: payload)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(4)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(payload[:content])
expect(json_response['slug']).to eq(payload[:title].tr(' ', '-'))
expect(json_response['title']).to eq(payload[:title])
end
[:title, :content, :format].each do |part|
it "updates with wiki with missing #{part}" do
payload.delete(part)
put(api(url, user), params: payload)
expect(response).to have_gitlab_http_status(:ok)
end
end
end
RSpec.shared_examples_for 'wiki API 403 Forbidden' do
it 'returns 403 Forbidden' do
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response.size).to eq(1)
expect(json_response['message']).to eq('403 Forbidden')
end
end
RSpec.shared_examples_for 'wiki API 404 Wiki Page Not Found' do
it 'returns 404 Wiki Page Not Found' do
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response.size).to eq(1)
expect(json_response['message']).to eq('404 Wiki Page Not Found')
end
end
RSpec.shared_examples_for 'wiki API 404 Not Found' do |what|
it "returns 404 #{what} Not Found" do
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response.size).to eq(1)
expect(json_response['message']).to eq("404 #{what} Not Found")
end
end
RSpec.shared_examples_for 'wiki API 204 No Content' do
it 'returns 204 No Content' do
expect(response).to have_gitlab_http_status(:no_content)
end
end
RSpec.shared_examples_for 'wiki API uploads wiki attachment' do
it 'pushes attachment to the wiki repository' do
allow(SecureRandom).to receive(:hex).and_return('fixed_hex')
workhorse_post_with_file(api(url, user), file_key: :file, params: payload)
expect(response).to have_gitlab_http_status(:created)
expect(json_response).to eq result_hash.deep_stringify_keys
end
it 'responds with validation error on empty file' do
payload.delete(:file)
post(api(url, user), params: payload)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response.size).to eq(1)
expect(json_response['error']).to eq('file is missing')
end
it 'responds with validation error on invalid temp file' do
payload[:file] = { tempfile: '/etc/hosts' }
post(api(url, user), params: payload)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response.size).to eq(1)
expect(json_response['error']).to eq('file is invalid')
end
it 'is backward compatible with regular multipart uploads' do
allow(SecureRandom).to receive(:hex).and_return('fixed_hex')
post(api(url, user), params: payload)
expect(response).to have_gitlab_http_status(:created)
expect(json_response).to eq result_hash.deep_stringify_keys
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