diff --git a/docs/api/apiv3/paths/work_package_file_links.yml b/docs/api/apiv3/paths/work_package_file_links.yml index 3665252d10..5c2fbc9872 100644 --- a/docs/api/apiv3/paths/work_package_file_links.yml +++ b/docs/api/apiv3/paths/work_package_file_links.yml @@ -11,10 +11,10 @@ post: The request body must contain the generated storage token, and not the storage id. In addition, the request must send the origin file id and the stored meta data. - - If there is already a link on the given work package with the same file id from the same storage, the previous - data is not overwritten. - + + Up to 20 file links can be submitted at once. If there is already a link on the given work package with the same + file id from the same storage, the previous data is not overwritten. + As it is possible to link folders from a storage to a work package, the _mimeType_ of this entity can be empty. parameters: - name: id @@ -44,7 +44,7 @@ post: _links: storageUrl: href: https://nextcloud.deathstar.rocks/ - + responses: '201': description: Created @@ -137,9 +137,9 @@ get: message: You are not authorized to access this resource. description: |- Returned if the client does not have sufficient permissions. - + **Required permission:** view file links - + *Note that you will only receive this error, if you are at least allowed to see the corresponding work package.* '404': content: diff --git a/lib/api/utilities/endpoints/bodied.rb b/lib/api/utilities/endpoints/bodied.rb index 65a4e72558..14f7d50987 100644 --- a/lib/api/utilities/endpoints/bodied.rb +++ b/lib/api/utilities/endpoints/bodied.rb @@ -84,7 +84,7 @@ module API endpoint = self -> do - request = self # proc is executed in the context of the request + request = self # proc is executed in the context of the grape request endpoint.before_hook&.(request: request) params = endpoint.parse(request) call = endpoint.process(request, params) diff --git a/modules/storages/config/locales/en.yml b/modules/storages/config/locales/en.yml index 1ba9c95c45..8e4d7e9667 100644 --- a/modules/storages/config/locales/en.yml +++ b/modules/storages/config/locales/en.yml @@ -25,6 +25,10 @@ en: minimal_nextcloud_version_unmet: "does not meet minimal version requirements (must be Nextcloud 23 or higher)" not_nextcloud_server: "is not a Nextcloud server" + api_v3: + errors: + too_many_elements_created_at_once: "Too many elements created at once. Expected %{max} at most, got %{actual}." + storages: page_titles: project_settings: diff --git a/modules/storages/lib/api/v3/file_links/parse_create_params_service.rb b/modules/storages/lib/api/v3/file_links/parse_create_params_service.rb index 3856affa88..4b87af1dd7 100644 --- a/modules/storages/lib/api/v3/file_links/parse_create_params_service.rb +++ b/modules/storages/lib/api/v3/file_links/parse_create_params_service.rb @@ -28,28 +28,58 @@ module API::V3::FileLinks class ParseCreateParamsService < ::API::ParseResourceParamsService + MAX_ELEMENTS = 20 + + attr_reader :request_body + def call(request_body) + @request_body = request_body ServiceResult.new( success: true, - result: parse_elements(request_body) + result: parse_elements ) end - def parse_elements(request_body) - request_body.dig("_embedded", "elements") - .tap { ensure_valid_elements(_1) } - .map do |params| + private + + def parse_elements + assert_valid_elements + + elements.map do |element| API::V3::FileLinks::FileLinkRepresenter .new(Hashie::Mash.new, current_user: current_user) - .from_hash(params) + .from_hash(element) end end - private + def elements + @elements ||= request_body.dig("_embedded", "elements") + end + + def assert_valid_elements + assert_elements_is_present + assert_elements_is_an_array + assert_elements_does_not_exceed_maximum + end + + def assert_elements_is_present + return if elements.present? + + raise API::Errors::PropertyMissingError.new('_embedded/elements') + end + + def assert_elements_is_an_array + return if elements.is_a?(Array) + + raise API::Errors::PropertyFormatError.new('_embedded/elements', 'Array', elements.class.name) + end + + def assert_elements_does_not_exceed_maximum + return if elements.size <= MAX_ELEMENTS - def ensure_valid_elements(elements) - raise API::Errors::PropertyMissingError.new('_embedded/elements') if elements.blank? - raise API::Errors::PropertyFormatError.new('_embedded/elements', 'Array', elements.class.name) unless elements.is_a?(Array) + raise API::Errors::Validation.new('_embedded/elements', + I18n.t('api_v3.errors.too_many_elements_created_at_once', + max: MAX_ELEMENTS, actual: elements.size)) end end end diff --git a/modules/storages/spec/requests/api/v3/file_links/file_links_spec.rb b/modules/storages/spec/requests/api/v3/file_links/file_links_spec.rb index 9be98e9c87..3b5c8a46f9 100644 --- a/modules/storages/spec/requests/api/v3/file_links/file_links_spec.rb +++ b/modules/storages/spec/requests/api/v3/file_links/file_links_spec.rb @@ -183,6 +183,42 @@ describe 'API v3 file links resource', type: :request do expected_format: 'Array', actual: 'Integer') end + + # rubocop:disable RSpec/MultipleMemoizedHelpers + context "when more than #{API::V3::FileLinks::ParseCreateParamsService::MAX_ELEMENTS} embedded elements" do + let(:max) { API::V3::FileLinks::ParseCreateParamsService::MAX_ELEMENTS } + let(:too_many) { max + 1 } + let(:params) do + { + _type: "Collection", + _embedded: { + elements: 1.upto(too_many).map do |id| + { + originData: { + id: id, + name: "logo#{id}.png", + mimeType: "image/png", + createdAt: "2021-12-19T09:42:10.170Z", + lastModifiedAt: "2021-12-20T14:00:13.987Z", + createdByName: "Luke Skywalker", + lastModifiedByName: "Anakin Skywalker" + }, + _links: { + storageUrl: { + href: storage_url1 + } + } + } + end + } + } + end + + it_behaves_like 'constraint violation' do + let(:message) { "Too many elements created at once. Expected #{max} at most, got #{too_many}." } + end + end + # rubocop:enable RSpec/MultipleMemoizedHelpers end describe 'GET /api/v3/file_links/:file_link_id' do