From e711b17bed681fd100303e63ff80af9a6de0e159 Mon Sep 17 00:00:00 2001 From: Bruno Pagno Date: Mon, 9 Dec 2024 17:34:54 +0100 Subject: [PATCH] implement API for creating folt statusders --- .../storages/create_folder_service.rb | 77 ++++++++++ .../v3/storage_files/storage_folders_api.rb | 66 ++++++++ .../lib/api/v3/storages/storages_api.rb | 1 + .../lib/open_project/storages/engine.rb | 4 + .../v3/storages/storage_folders_api_spec.rb | 144 ++++++++++++++++++ .../storages/create_folder_service_spec.rb | 112 ++++++++++++++ 6 files changed, 404 insertions(+) create mode 100644 modules/storages/app/services/storages/create_folder_service.rb create mode 100644 modules/storages/lib/api/v3/storage_files/storage_folders_api.rb create mode 100644 modules/storages/spec/requests/api/v3/storages/storage_folders_api_spec.rb create mode 100644 modules/storages/spec/services/storages/create_folder_service_spec.rb 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..8c9f2ed1d352 --- /dev/null +++ b/modules/storages/app/services/storages/create_folder_service.rb @@ -0,0 +1,77 @@ +# 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 Storages + class CreateFolderService < BaseService + using Peripherals::ServiceResultRefinements + + def self.call(storage:, user:, name:, parent_id:) + new.call(storage:, user:, name:, parent_id:) + end + + def call(storage:, user:, name:, parent_id:) + auth_strategy = Peripherals::Registry.resolve("#{storage}.authentication.user_bound").call(user: user) + + Peripherals::Registry + .resolve("#{storage}.commands.create_folder") + .call( + storage:, + auth_strategy:, + folder_name: name, + parent_location: parent_path(parent_id, storage, user) + ) + end + + private + + def parent_path(parent_id, storage, user) + case storage.short_provider_type + when "nextcloud" + location_from_file_info(parent_id, storage, user) + when "one_drive" + Peripherals::ParentFolder.new(parent_id) + else + raise "Unknown Storage Type" + end + end + + def location_from_file_info(parent_id, storage, user) + StorageFileService + .call(storage: storage, user: user, file_id: parent_id) + .match( + on_success: lambda { |folder_info| + path = URI.decode_uri_component(folder_info.location) + Peripherals::ParentFolder.new(path) + }, + on_failure: ->(error) { raise error } + ) + 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..9766923a5e05 --- /dev/null +++ b/modules/storages/lib/api/v3/storage_files/storage_folders_api.rb @@ -0,0 +1,66 @@ +# 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::StorageErrorHelper + + resources :folders do + params do + requires :name, type: String, desc: "Folder name" + requires :parent_id, type: String, desc: "Id of the parent folder" + end + + post do + ::Storages::CreateFolderService.call( + storage: @storage, + user: current_user, + name: params["name"], + 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..fddfa2a9bdcb --- /dev/null +++ b/modules/storages/spec/requests/api/v3/storages/storage_folders_api_spec.rb @@ -0,0 +1,144 @@ +# 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: file_info.id, 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 + file_info_mock = class_double(Storages::Peripherals::StorageInteraction::Nextcloud::FileInfoQuery) + allow(file_info_mock).to receive(:call).with( + storage: storage, + auth_strategy: instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), + file_id: file_info.id + ).and_return(ServiceResult.success(result: file_info)) + Storages::Peripherals::Registry.stub("nextcloud.queries.file_info", file_info_mock) + end + + context "with successful response" do + subject { last_response.body } + + before do + create_folder_mock = class_double(Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand) + allow(create_folder_mock).to receive(:call).with( + storage: storage, + auth_strategy: instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), + folder_name:, + parent_location: instance_of(Storages::Peripherals::ParentFolder) + ).and_return(ServiceResult.success(result: response)) + Storages::Peripherals::Registry.stub("nextcloud.commands.create_folder", create_folder_mock) + 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 + create_folder_mock = class_double(Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand) + allow(create_folder_mock).to receive(:call).with( + storage: storage, + auth_strategy: instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), + folder_name:, + parent_location: instance_of(Storages::Peripherals::ParentFolder) + ).and_return(ServiceResult.failure(result: error, errors: Storages::StorageError.new(code: error))) + Storages::Peripherals::Registry.stub("nextcloud.commands.create_folder", create_folder_mock) + 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 diff --git a/modules/storages/spec/services/storages/create_folder_service_spec.rb b/modules/storages/spec/services/storages/create_folder_service_spec.rb new file mode 100644 index 000000000000..d4c6a96ce2d0 --- /dev/null +++ b/modules/storages/spec/services/storages/create_folder_service_spec.rb @@ -0,0 +1,112 @@ +# 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 Storages::CreateFolderService do + subject(:service) { described_class.call(storage:, user:, name:, parent_id:) } + + let(:user) { create(:admin) } + let(:name) { "TestFolderName" } + + context "when storage is nextcloud" do + let(:storage) { create(:nextcloud_storage) } + let(:parent_id) { file_info.id } + + let(:file_info) do + Storages::StorageFileInfo.new( + status: "OK", + status_code: 200, + id: SecureRandom.hex, + name: "/", + location: "/Path/To/Parent/Next" + ) + end + + let(:create_folder_command) { class_double(Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand) } + + before do + file_info_mock = class_double(Storages::Peripherals::StorageInteraction::Nextcloud::FileInfoQuery) + allow(file_info_mock).to receive(:call).with( + storage: storage, + auth_strategy: instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), + file_id: file_info.id + ).and_return(ServiceResult.success(result: file_info)) + Storages::Peripherals::Registry.stub("nextcloud.queries.file_info", file_info_mock) + + allow(create_folder_command).to receive(:call).and_return(ServiceResult.success) + Storages::Peripherals::Registry.stub("nextcloud.commands.create_folder", create_folder_command) + end + + it "calls the appropriate command with the expected parameters" do + service + + expect(create_folder_command).to have_received(:call).with( + storage:, + auth_strategy: instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), + folder_name: name, + parent_location: Storages::Peripherals::ParentFolder.new(file_info.location) + ).once + end + end + + context "when storage is one_drive" do + let(:storage) { create(:one_drive_storage) } + let(:parent_id) { file_info.id } + + let(:file_info) do + Storages::StorageFileInfo.new( + status: "OK", + status_code: 200, + id: "/Path/To/Parent/One", + name: "/" + ) + end + + let(:create_folder_command) { class_double(Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand) } + + before do + allow(create_folder_command).to receive(:call).and_return(ServiceResult.success) + Storages::Peripherals::Registry.stub("one_drive.commands.create_folder", create_folder_command) + end + + it "calls the appropriate command with the expected parameters" do + service + + expect(create_folder_command).to have_received(:call).with( + storage:, + auth_strategy: instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), + folder_name: name, + parent_location: Storages::Peripherals::ParentFolder.new(file_info.id) + ).once + end + end +end