diff --git a/modules/storages/app/services/storages/create_folder_service.rb b/modules/storages/app/services/storages/create_folder_service.rb new file mode 100644 index 000000000000..3bdd151a48c6 --- /dev/null +++ b/modules/storages/app/services/storages/create_folder_service.rb @@ -0,0 +1,43 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Storages + class CreateFolderService < BaseService + def self.call(storage:, user:, name:, parent_location:) + new.call(storage:, user:, name:, parent_location:) + end + + def call(storage:, user:, name:, parent_location:) + auth_strategy = Peripherals::Registry.resolve("#{storage}.authentication.user_bound").call(user: user) + + ::Storages::Peripherals::Registry + .resolve("#{storage}.commands.create_folder") + .call(storage:, auth_strategy:, folder_name: name, parent_location:) + end + end +end diff --git a/modules/storages/lib/api/v3/storage_files/storage_folders_api.rb b/modules/storages/lib/api/v3/storage_files/storage_folders_api.rb new file mode 100644 index 000000000000..5eeb76ba67bb --- /dev/null +++ b/modules/storages/lib/api/v3/storage_files/storage_folders_api.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module API + module V3 + module StorageFiles + class StorageFoldersAPI < ::API::OpenProjectAPI + using ::Storages::Peripherals::ServiceResultRefinements + + helpers ::Storages::Peripherals::StorageFileInfoConverter, + ::Storages::Peripherals::StorageErrorHelper + + helpers do + def parent_path_from_parent_id(parent_id) + ::Storages::StorageFileService + .call(storage: @storage, user: current_user, file_id: parent_id) + .match( + on_success: lambda { |folder_info| + path = URI.decode_uri_component(folder_info.location) + ::Storages::Peripherals::ParentFolder.new(path) + }, + on_failure: ->(error) { raise_error(error) } + ) + end + end + + resources :folders do + params do + requires :name, type: String, desc: "Folder name" + end + + post do + ::Storages::CreateFolderService.call( + storage: @storage, + user: current_user, + name: params["name"], + parent_location: parent_path_from_parent_id(params["parent_id"]) + ).match( + on_success: lambda { |storage_folder| + API::V3::StorageFiles::StorageFileRepresenter.new( + storage_folder, + @storage, + current_user: + ) + }, + on_failure: ->(error) { raise_error(error) } + ) + end + end + end + end + end +end diff --git a/modules/storages/lib/api/v3/storages/storages_api.rb b/modules/storages/lib/api/v3/storages/storages_api.rb index b5dedbcd20e7..f77b67fa4d24 100644 --- a/modules/storages/lib/api/v3/storages/storages_api.rb +++ b/modules/storages/lib/api/v3/storages/storages_api.rb @@ -50,6 +50,7 @@ class API::V3::Storages::StoragesAPI < API::OpenProjectAPI mount API::V3::StorageFiles::StorageFilesAPI mount API::V3::OAuthClient::OAuthClientCredentialsAPI mount API::V3::Storages::StorageOpenAPI + mount API::V3::StorageFiles::StorageFoldersAPI end end end diff --git a/modules/storages/lib/open_project/storages/engine.rb b/modules/storages/lib/open_project/storages/engine.rb index 1a40a918da0b..d60979349731 100644 --- a/modules/storages/lib/open_project/storages/engine.rb +++ b/modules/storages/lib/open_project/storages/engine.rb @@ -312,6 +312,10 @@ def self.external_file_permissions "#{storage_files(storage_id)}/#{file_id}" end + add_api_path :storage_folders do |storage_id| + "#{storage(storage_id)}/folders" + end + add_api_path :prepare_upload do |storage_id| "#{storage(storage_id)}/files/prepare_upload" end diff --git a/modules/storages/spec/requests/api/v3/storages/storage_folders_api_spec.rb b/modules/storages/spec/requests/api/v3/storages/storage_folders_api_spec.rb new file mode 100644 index 000000000000..a2999475400b --- /dev/null +++ b/modules/storages/spec/requests/api/v3/storages/storage_folders_api_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require_module_spec_helper + +RSpec.describe "API v3 storage folders", :webmock, content_type: :json do + include API::V3::Utilities::PathHelper + include StorageServerHelpers + + let(:permissions) { %i(view_work_packages view_file_links manage_file_links) } + let(:project) { create(:project) } + + let(:current_user) do + create(:user, member_with_permissions: { project => permissions }) + end + + let(:oauth_application) { create(:oauth_application) } + let(:storage) { create(:nextcloud_storage_configured, creator: current_user, oauth_application:) } + let(:oauth_token) { create(:oauth_client_token, user: current_user, oauth_client: storage.oauth_client) } + let(:project_storage) { create(:project_storage, project:, storage:) } + + subject(:last_response) { post(path, body) } + + before do + oauth_application + project_storage + login_as current_user + end + + describe "POST /api/v3/storages/:storage_id/folders" do + let(:path) { api_v3_paths.storage_folders(storage.id) } + let(:body) { { parent_id: "1234", name: folder_name }.to_json } + let(:folder_name) { "TestFolder" } + + let(:response) do + Storages::StorageFile.new( + id: 1, + name: folder_name, + size: 128, + mime_type: "application/x-op-directory", + created_at: DateTime.now, + last_modified_at: DateTime.now, + created_by_name: "Obi-Wan Kenobi", + last_modified_by_name: "Obi-Wan Kenobi", + location: "/", + permissions: %i[readable] + ) + end + + let(:file_info) do + Storages::StorageFileInfo.new( + status: "OK", + status_code: 200, + id: SecureRandom.hex, + name: "/", + location: "/" + ) + end + + before do + Storages::Peripherals::Registry.stub( + "nextcloud.queries.file_info", + ->(_) { ServiceResult.success(result: file_info) } + ) + end + + context "with successful response" do + subject { last_response.body } + + before do + Storages::Peripherals::Registry.stub( + "nextcloud.commands.create_folder", + ->(_) { ServiceResult.success(result: response) } + ) + end + + it "responds with appropriate JSON" do + expect(subject).to be_json_eql(response.id.to_json).at_path("id") + expect(subject).to be_json_eql(response.name.to_json).at_path("name") + expect(subject).to be_json_eql(response.permissions.to_json).at_path("permissions") + end + end + + context "with query failed" do + before do + Storages::Peripherals::Registry.stub( + "nextcloud.commands.create_folder", + ->(_) { ServiceResult.failure(result: error, errors: Storages::StorageError.new(code: error)) } + ) + end + + context "with authorization failure" do + let(:error) { :unauthorized } + + it { expect(last_response).to have_http_status(:internal_server_error) } + end + + context "with internal error" do + let(:error) { :error } + + it { expect(last_response).to have_http_status(:internal_server_error) } + end + end + end +end