mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Merge pull request #15407 from opf/implementation/54355-use-authentication-in-folderfilesfileidsdeepquery
[#54355] Use authentication in FolderFilesFileIdsDeepQuery
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:latest
|
||||
|
||||
@@ -38,7 +38,7 @@ module Storages
|
||||
register(:file_info, StorageInteraction::Nextcloud::FileInfoQuery)
|
||||
register(:files_info, StorageInteraction::Nextcloud::FilesInfoQuery)
|
||||
register(:files, StorageInteraction::Nextcloud::FilesQuery)
|
||||
register(:folder_files_file_ids_deep_query, StorageInteraction::Nextcloud::FolderFilesFileIdsDeepQuery)
|
||||
register(:folder_files_file_ids_deep, StorageInteraction::Nextcloud::FolderFilesFileIdsDeepQuery)
|
||||
register(:propfind, StorageInteraction::Nextcloud::Internal::PropfindQuery)
|
||||
register(:group_users, StorageInteraction::Nextcloud::GroupUsersQuery)
|
||||
register(:upload_link, StorageInteraction::Nextcloud::UploadLinkQuery)
|
||||
|
||||
@@ -38,7 +38,7 @@ module Storages
|
||||
register(:file_info, StorageInteraction::OneDrive::FileInfoQuery)
|
||||
register(:files_info, StorageInteraction::OneDrive::FilesInfoQuery)
|
||||
register(:open_file_link, StorageInteraction::OneDrive::OpenFileLinkQuery)
|
||||
register(:folder_files_file_ids_deep_query, StorageInteraction::OneDrive::FolderFilesFileIdsDeepQuery)
|
||||
register(:folder_files_file_ids_deep, StorageInteraction::OneDrive::FolderFilesFileIdsDeepQuery)
|
||||
register(:open_storage, StorageInteraction::OneDrive::OpenStorageQuery)
|
||||
register(:upload_link, StorageInteraction::OneDrive::UploadLinkQuery)
|
||||
end
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
module Storages::Peripherals::StorageInteraction::Nextcloud
|
||||
class FileIdsQuery
|
||||
def initialize(storage)
|
||||
@query = ::Storages::Peripherals::StorageInteraction::Nextcloud::Internal::PropfindQuery.new(storage)
|
||||
@query = ::Storages::Peripherals::StorageInteraction::Nextcloud::Internal::PropfindQueryLegacy.new(storage)
|
||||
end
|
||||
|
||||
def self.call(storage:, path:)
|
||||
|
||||
+38
-13
@@ -28,19 +28,44 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Storages::Peripherals::StorageInteraction::Nextcloud
|
||||
class FolderFilesFileIdsDeepQuery
|
||||
def self.call(storage:, folder:)
|
||||
::Storages::Peripherals::Registry
|
||||
.resolve("nextcloud.queries.propfind")
|
||||
.call(
|
||||
storage:,
|
||||
depth: "infinity",
|
||||
path: folder.path,
|
||||
# nc:acl-list is only required to avoid https://community.openproject.org/wp/49628. See comment #4.
|
||||
props: %w[oc:fileid nc:acl-list]
|
||||
).map do |obj|
|
||||
obj.transform_values { |value| Storages::StorageFileInfo.from_id(value["fileid"]) }
|
||||
module Storages
|
||||
module Peripherals
|
||||
module StorageInteraction
|
||||
module Nextcloud
|
||||
class FolderFilesFileIdsDeepQuery
|
||||
def self.call(storage:, auth_strategy:, folder:)
|
||||
new(storage).call(auth_strategy:, folder:)
|
||||
end
|
||||
|
||||
def initialize(storage)
|
||||
@storage = storage
|
||||
@propfind_query = Internal::PropfindQuery.new(storage)
|
||||
end
|
||||
|
||||
def call(auth_strategy:, folder:)
|
||||
origin_user_id = Util.origin_user_id(caller: self.class, storage: @storage, auth_strategy:)
|
||||
.on_failure do |result|
|
||||
return result
|
||||
end
|
||||
|
||||
Authentication[auth_strategy].call(storage: @storage, http_options:) do |http|
|
||||
# nc:acl-list is only required to avoid https://community.openproject.org/wp/49628. See comment #4.
|
||||
@propfind_query.call(http:,
|
||||
username: origin_user_id.result,
|
||||
path: folder.path,
|
||||
props: %w[oc:fileid nc:acl-list])
|
||||
.map do |obj|
|
||||
obj.transform_values { |value| StorageFileId.new(id: value["fileid"]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def http_options
|
||||
Util.webdav_request_with_depth("infinity")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+117
-100
@@ -28,114 +28,131 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Storages::Peripherals::StorageInteraction::Nextcloud::Internal
|
||||
class PropfindQuery
|
||||
UTIL = ::Storages::Peripherals::StorageInteraction::Nextcloud::Util
|
||||
module Storages
|
||||
module Peripherals
|
||||
module StorageInteraction
|
||||
module Nextcloud
|
||||
module Internal
|
||||
class PropfindQuery
|
||||
# Only for information purposes currently.
|
||||
# Probably a bit later we could validate `#call` parameters.
|
||||
#
|
||||
# DEPTH = %w[0 1 infinity].freeze
|
||||
# POSSIBLE_PROPS = %w[
|
||||
# d:getlastmodified
|
||||
# d:getetag
|
||||
# d:getcontenttype
|
||||
# d:resourcetype
|
||||
# d:getcontentlength
|
||||
# d:permissions
|
||||
# d:size
|
||||
# oc:id
|
||||
# oc:fileid
|
||||
# oc:favorite
|
||||
# oc:comments-href
|
||||
# oc:comments-count
|
||||
# oc:comments-unread
|
||||
# oc:owner-id
|
||||
# oc:owner-display-name
|
||||
# oc:share-types
|
||||
# oc:checksums
|
||||
# oc:size
|
||||
# nc:has-preview
|
||||
# nc:rich-workspace
|
||||
# nc:contained-folder-count
|
||||
# nc:contained-file-count
|
||||
# nc:acl-list
|
||||
# ].freeze
|
||||
|
||||
# Only for information purposes currently.
|
||||
# Probably a bit later we could validate `#call` parameters.
|
||||
#
|
||||
# DEPTH = %w[0 1 infinity].freeze
|
||||
# POSSIBLE_PROPS = %w[
|
||||
# d:getlastmodified
|
||||
# d:getetag
|
||||
# d:getcontenttype
|
||||
# d:resourcetype
|
||||
# d:getcontentlength
|
||||
# d:permissions
|
||||
# d:size
|
||||
# oc:id
|
||||
# oc:fileid
|
||||
# oc:favorite
|
||||
# oc:comments-href
|
||||
# oc:comments-count
|
||||
# oc:comments-unread
|
||||
# oc:owner-id
|
||||
# oc:owner-display-name
|
||||
# oc:share-types
|
||||
# oc:checksums
|
||||
# oc:size
|
||||
# nc:has-preview
|
||||
# nc:rich-workspace
|
||||
# nc:contained-folder-count
|
||||
# nc:contained-file-count
|
||||
# nc:acl-list
|
||||
# ].freeze
|
||||
def self.call(storage:, http:, username:, path:, props:)
|
||||
new(storage).call(http:, username:, path:, props:)
|
||||
end
|
||||
|
||||
def initialize(storage)
|
||||
@uri = storage.uri
|
||||
@username = storage.username
|
||||
@password = storage.password
|
||||
@group = storage.group
|
||||
end
|
||||
def initialize(storage)
|
||||
@storage = storage
|
||||
end
|
||||
|
||||
def self.call(storage:, depth:, path:, props:)
|
||||
new(storage).call(depth:, path:, props:)
|
||||
end
|
||||
def call(http:, username:, path:, props:)
|
||||
request_uri = Util.join_uri_path(base_uri, CGI.escapeURIComponent(username), Util.escape_path(path))
|
||||
response = http.request(:propfind, request_uri, xml: request_body(props))
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def call(depth:, path:, props:)
|
||||
body = Nokogiri::XML::Builder.new do |xml|
|
||||
xml["d"].propfind(
|
||||
"xmlns:d" => "DAV:",
|
||||
"xmlns:oc" => "http://owncloud.org/ns",
|
||||
"xmlns:nc" => "http://nextcloud.org/ns"
|
||||
) do
|
||||
xml["d"].prop do
|
||||
props.each do |prop|
|
||||
namespace, property = prop.split(":")
|
||||
xml[namespace].send(property)
|
||||
handle_response(response, username)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_response(response, username)
|
||||
case response
|
||||
in { status: 200..299 }
|
||||
success_result(response, username)
|
||||
in { status: 401 }
|
||||
Util.failure(code: :unauthorized,
|
||||
data: Util.error_data_from_response(caller: self.class, response:),
|
||||
log_message: "Outbound request not authorized!")
|
||||
in { status: 404 }
|
||||
Util.failure(code: :not_found,
|
||||
data: Util.error_data_from_response(caller: self.class, response:),
|
||||
log_message: "Outbound request destination not found!")
|
||||
in { status: 405 }
|
||||
Util.failure(code: :not_allowed,
|
||||
data: Util.error_data_from_response(caller: self.class, response:),
|
||||
log_message: "Outbound request method not allowed!")
|
||||
|
||||
else
|
||||
Util.failure(code: :error,
|
||||
data: Util.error_data_from_response(caller: self.class, response:),
|
||||
log_message: "Outbound request failed with unknown error!")
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def success_result(response, username)
|
||||
doc = Nokogiri::XML(response.body.to_s)
|
||||
result = {}
|
||||
doc.xpath("/d:multistatus/d:response").each do |resource_section|
|
||||
resource = resource_path(resource_section, username)
|
||||
result[resource] = {}
|
||||
|
||||
# In future it could be useful to respond not only with found, but not found props as well
|
||||
# resource_section.xpath("d:propstat[d:status[text() = 'HTTP/1.1 404 Not Found']]/d:prop/*")
|
||||
resource_section.xpath("d:propstat[d:status[text() = 'HTTP/1.1 200 OK']]/d:prop/*").each do |node|
|
||||
result[resource][node.name.to_s] = node.text.strip
|
||||
end
|
||||
end
|
||||
|
||||
ServiceResult.success(result:)
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def base_uri = "#{@storage.uri}remote.php/dav/files"
|
||||
|
||||
def resource_path(section, username)
|
||||
path = CGI.unescape(section.xpath("d:href").text.strip)
|
||||
.gsub!(Util.join_uri_path(URI(base_uri).path, username), "")
|
||||
|
||||
path.end_with?("/") && path.length > 1 ? path.chop : path
|
||||
end
|
||||
|
||||
def request_body(props)
|
||||
Nokogiri::XML::Builder.new do |xml|
|
||||
xml["d"].propfind(
|
||||
"xmlns:d" => "DAV:",
|
||||
"xmlns:oc" => "http://owncloud.org/ns",
|
||||
"xmlns:nc" => "http://nextcloud.org/ns"
|
||||
) do
|
||||
xml["d"].prop do
|
||||
props.each do |prop|
|
||||
namespace, property = prop.split(":")
|
||||
xml[namespace].send(property)
|
||||
end
|
||||
end
|
||||
end
|
||||
end.to_xml
|
||||
end
|
||||
end
|
||||
end
|
||||
end.to_xml
|
||||
|
||||
response = OpenProject
|
||||
.httpx
|
||||
.basic_auth(@username, @password)
|
||||
.with(headers: { "Depth" => depth })
|
||||
.request(
|
||||
"PROPFIND",
|
||||
UTIL.join_uri_path(
|
||||
@uri,
|
||||
"remote.php/dav/files",
|
||||
CGI.escapeURIComponent(@username),
|
||||
UTIL.escape_path(path)
|
||||
),
|
||||
xml: body
|
||||
)
|
||||
|
||||
error_data = Storages::StorageErrorData.new(source: self.class, payload: response)
|
||||
|
||||
case response
|
||||
in { status: 200..299 }
|
||||
doc = Nokogiri::XML(response.body.to_s)
|
||||
result = {}
|
||||
doc.xpath("/d:multistatus/d:response").each do |resource_section|
|
||||
resource = CGI.unescape(resource_section.xpath("d:href").text.strip)
|
||||
.gsub!(UTIL.join_uri_path(@uri.path, "/remote.php/dav/files/#{@username}/"), "")
|
||||
|
||||
result[resource] = {}
|
||||
|
||||
# In future it could be useful to respond not only with found, but not found props as well
|
||||
# resource_section.xpath("d:propstat[d:status[text() = 'HTTP/1.1 404 Not Found']]/d:prop/*")
|
||||
resource_section.xpath("d:propstat[d:status[text() = 'HTTP/1.1 200 OK']]/d:prop/*").each do |node|
|
||||
result[resource][node.name.to_s] = node.text.strip
|
||||
end
|
||||
end
|
||||
|
||||
ServiceResult.success(result:)
|
||||
in { status: 405 }
|
||||
UTIL.error(:not_allowed, "Outbound request method not allowed", error_data)
|
||||
in { status: 401 }
|
||||
UTIL.error(:unauthorized, "Outbound request not authorized", error_data)
|
||||
in { status: 404 }
|
||||
UTIL.error(:not_found, "Outbound request destination not found", error_data)
|
||||
else
|
||||
UTIL.error(:error, "Outbound request failed", error_data)
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
end
|
||||
end
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2024 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::Peripherals::StorageInteraction::Nextcloud::Internal
|
||||
class PropfindQueryLegacy
|
||||
UTIL = ::Storages::Peripherals::StorageInteraction::Nextcloud::Util
|
||||
|
||||
# Only for information purposes currently.
|
||||
# Probably a bit later we could validate `#call` parameters.
|
||||
#
|
||||
# DEPTH = %w[0 1 infinity].freeze
|
||||
# POSSIBLE_PROPS = %w[
|
||||
# d:getlastmodified
|
||||
# d:getetag
|
||||
# d:getcontenttype
|
||||
# d:resourcetype
|
||||
# d:getcontentlength
|
||||
# d:permissions
|
||||
# d:size
|
||||
# oc:id
|
||||
# oc:fileid
|
||||
# oc:favorite
|
||||
# oc:comments-href
|
||||
# oc:comments-count
|
||||
# oc:comments-unread
|
||||
# oc:owner-id
|
||||
# oc:owner-display-name
|
||||
# oc:share-types
|
||||
# oc:checksums
|
||||
# oc:size
|
||||
# nc:has-preview
|
||||
# nc:rich-workspace
|
||||
# nc:contained-folder-count
|
||||
# nc:contained-file-count
|
||||
# nc:acl-list
|
||||
# ].freeze
|
||||
|
||||
def initialize(storage)
|
||||
@uri = storage.uri
|
||||
@username = storage.username
|
||||
@password = storage.password
|
||||
@group = storage.group
|
||||
end
|
||||
|
||||
def self.call(storage:, depth:, path:, props:)
|
||||
new(storage).call(depth:, path:, props:)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def call(depth:, path:, props:)
|
||||
body = Nokogiri::XML::Builder.new do |xml|
|
||||
xml["d"].propfind(
|
||||
"xmlns:d" => "DAV:",
|
||||
"xmlns:oc" => "http://owncloud.org/ns",
|
||||
"xmlns:nc" => "http://nextcloud.org/ns"
|
||||
) do
|
||||
xml["d"].prop do
|
||||
props.each do |prop|
|
||||
namespace, property = prop.split(":")
|
||||
xml[namespace].send(property)
|
||||
end
|
||||
end
|
||||
end
|
||||
end.to_xml
|
||||
|
||||
response = OpenProject
|
||||
.httpx
|
||||
.basic_auth(@username, @password)
|
||||
.with(headers: { "Depth" => depth })
|
||||
.request(
|
||||
"PROPFIND",
|
||||
UTIL.join_uri_path(
|
||||
@uri,
|
||||
"remote.php/dav/files",
|
||||
CGI.escapeURIComponent(@username),
|
||||
UTIL.escape_path(path)
|
||||
),
|
||||
xml: body
|
||||
)
|
||||
|
||||
error_data = Storages::StorageErrorData.new(source: self.class, payload: response)
|
||||
|
||||
case response
|
||||
in { status: 200..299 }
|
||||
doc = Nokogiri::XML(response.body.to_s)
|
||||
result = {}
|
||||
doc.xpath("/d:multistatus/d:response").each do |resource_section|
|
||||
resource = CGI.unescape(resource_section.xpath("d:href").text.strip)
|
||||
.gsub!(UTIL.join_uri_path(@uri.path, "/remote.php/dav/files/#{@username}/"), "")
|
||||
|
||||
result[resource] = {}
|
||||
|
||||
# In future it could be useful to respond not only with found, but not found props as well
|
||||
# resource_section.xpath("d:propstat[d:status[text() = 'HTTP/1.1 404 Not Found']]/d:prop/*")
|
||||
resource_section.xpath("d:propstat[d:status[text() = 'HTTP/1.1 200 OK']]/d:prop/*").each do |node|
|
||||
result[resource][node.name.to_s] = node.text.strip
|
||||
end
|
||||
end
|
||||
|
||||
ServiceResult.success(result:)
|
||||
in { status: 405 }
|
||||
UTIL.error(:not_allowed, "Outbound request method not allowed", error_data)
|
||||
in { status: 401 }
|
||||
UTIL.error(:unauthorized, "Outbound request not authorized", error_data)
|
||||
in { status: 404 }
|
||||
UTIL.error(:not_found, "Outbound request destination not found", error_data)
|
||||
else
|
||||
UTIL.error(:error, "Outbound request failed", error_data)
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
end
|
||||
end
|
||||
+75
-95
@@ -28,109 +28,89 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Storages::Peripherals::StorageInteraction::OneDrive
|
||||
class FolderFilesFileIdsDeepQuery
|
||||
FIELDS = %w[id name file folder parentReference].freeze
|
||||
module Storages
|
||||
module Peripherals
|
||||
module StorageInteraction
|
||||
module OneDrive
|
||||
class FolderFilesFileIdsDeepQuery
|
||||
CHILDREN_FIELDS = %w[id name file folder parentReference].freeze
|
||||
FOLDER_FIELDS = %w[id name parentReference].freeze
|
||||
|
||||
def self.call(storage:, folder:)
|
||||
new(storage).call(folder:)
|
||||
end
|
||||
def self.call(storage:, auth_strategy:, folder:)
|
||||
new(storage).call(auth_strategy:, folder:)
|
||||
end
|
||||
|
||||
def initialize(storage)
|
||||
@storage = storage
|
||||
@delegate = Internal::ChildrenQuery.new(storage)
|
||||
end
|
||||
def initialize(storage)
|
||||
@storage = storage
|
||||
@children_query = Internal::ChildrenQuery.new(storage)
|
||||
@drive_item_query = Internal::DriveItemQuery.new(storage)
|
||||
end
|
||||
|
||||
def call(folder:)
|
||||
Util.using_admin_token(@storage) do |http|
|
||||
fetch_result = fetch_folder(http, folder)
|
||||
return fetch_result if fetch_result.failure?
|
||||
def call(auth_strategy:, folder:)
|
||||
Authentication[auth_strategy].call(storage: @storage) do |http|
|
||||
fetch_result = fetch_folder(http, folder)
|
||||
return fetch_result if fetch_result.failure?
|
||||
|
||||
file_ids_dictionary = fetch_result.result
|
||||
queue = [folder]
|
||||
file_ids_dictionary = fetch_result.result
|
||||
queue = [folder]
|
||||
|
||||
while queue.any?
|
||||
dir = queue.shift
|
||||
while queue.any?
|
||||
dir = queue.shift
|
||||
|
||||
visit = visit(http, dir)
|
||||
return visit if visit.failure?
|
||||
visit = visit(http, dir)
|
||||
return visit if visit.failure?
|
||||
|
||||
entry, to_queue = visit.result.values_at(:entry, :to_queue)
|
||||
file_ids_dictionary = file_ids_dictionary.merge(entry)
|
||||
queue.concat(to_queue)
|
||||
entry, to_queue = visit.result.values_at(:entry, :to_queue)
|
||||
file_ids_dictionary = file_ids_dictionary.merge(entry)
|
||||
queue.concat(to_queue)
|
||||
end
|
||||
|
||||
ServiceResult.success(result: file_ids_dictionary)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def visit(http, folder)
|
||||
call = @children_query.call(http:, folder:, fields: CHILDREN_FIELDS)
|
||||
return call if call.failure?
|
||||
|
||||
entry = {}
|
||||
to_queue = []
|
||||
|
||||
call.result[:value].each do |json|
|
||||
new_entry, folder = parse_drive_item_info(json).values_at(:entry, :folder)
|
||||
|
||||
entry = entry.merge(new_entry)
|
||||
if folder.present?
|
||||
to_queue.append(folder)
|
||||
end
|
||||
end
|
||||
|
||||
ServiceResult.success(result: { entry:, to_queue: })
|
||||
end
|
||||
|
||||
def parse_drive_item_info(json)
|
||||
drive_item_id = json[:id]
|
||||
location = Util.extract_location(json[:parentReference], json[:name])
|
||||
|
||||
entry = { location => StorageFileId.new(id: drive_item_id) }
|
||||
folder = json[:folder].present? ? ParentFolder.new(drive_item_id) : nil
|
||||
|
||||
{ entry:, folder: }
|
||||
end
|
||||
|
||||
def fetch_folder(http, folder)
|
||||
result = @drive_item_query.call(http:, drive_item_id: folder.path, fields: FOLDER_FIELDS)
|
||||
result.map do |json|
|
||||
if folder.root?
|
||||
{ "/" => StorageFileId.new(id: json[:id]) }
|
||||
else
|
||||
parse_drive_item_info(json)[:entry]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ServiceResult.success(result: file_ids_dictionary)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def visit(http, folder)
|
||||
call = @delegate.call(http:, folder:, fields: FIELDS)
|
||||
return call if call.failure?
|
||||
|
||||
entry = {}
|
||||
to_queue = []
|
||||
|
||||
call.result[:value].each do |json|
|
||||
new_entry, folder = parse_drive_item_info(json).values_at(:entry, :folder)
|
||||
|
||||
entry = entry.merge(new_entry)
|
||||
if folder.present?
|
||||
to_queue.append(folder)
|
||||
end
|
||||
end
|
||||
|
||||
ServiceResult.success(result: { entry:, to_queue: })
|
||||
end
|
||||
|
||||
def parse_drive_item_info(json)
|
||||
drive_item_id = json[:id]
|
||||
location = Util.extract_location(json[:parentReference], json[:name])
|
||||
|
||||
entry = { location => Storages::StorageFileInfo.from_id(drive_item_id) }
|
||||
folder = json[:folder].present? ? Storages::Peripherals::ParentFolder.new(drive_item_id) : nil
|
||||
|
||||
{ entry:, folder: }
|
||||
end
|
||||
|
||||
# TODO: REMOVE WITH #51713, as this should be replaced by internal drive item query
|
||||
# with harmonized interface for authentication
|
||||
|
||||
def fetch_folder(http, folder)
|
||||
uri_path = if folder.root?
|
||||
"/v1.0/drives/#{@storage.drive_id}/root"
|
||||
else
|
||||
"/v1.0/drives/#{@storage.drive_id}/items/#{folder}"
|
||||
end
|
||||
|
||||
response = http.get(Util.join_uri_path(@storage.uri, "#{uri_path}?$select=id,name,parentReference"))
|
||||
handle_responses(response).map do |json|
|
||||
if folder.root?
|
||||
{ "/" => Storages::StorageFileInfo.from_id(json[:id]) }
|
||||
else
|
||||
parse_drive_item_info(json)[:entry]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_responses(response)
|
||||
case response
|
||||
in { status: 200..299 }
|
||||
ServiceResult.success(result: response.json(symbolize_keys: true))
|
||||
in { status: 404 }
|
||||
ServiceResult.failure(result: :not_found,
|
||||
errors: Util.storage_error(response:, code: :not_found, source: self))
|
||||
in { status: 403 }
|
||||
ServiceResult.failure(result: :forbidden,
|
||||
errors: Util.storage_error(response:, code: :forbidden, source: self))
|
||||
in { status: 401 }
|
||||
ServiceResult.failure(result: :unauthorized,
|
||||
errors: Util.storage_error(response:, code: :unauthorized, source: self))
|
||||
else
|
||||
data = ::Storages::StorageErrorData.new(source: self.class, payload: response)
|
||||
ServiceResult.failure(result: :error, errors: ::Storages::StorageError.new(code: :error, data:))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+6
-9
@@ -34,8 +34,6 @@ module Storages
|
||||
module OneDrive
|
||||
module Internal
|
||||
class DriveItemQuery
|
||||
Util = ::Storages::Peripherals::StorageInteraction::OneDrive::Util
|
||||
|
||||
def initialize(storage)
|
||||
@storage = storage
|
||||
end
|
||||
@@ -53,11 +51,10 @@ module Storages
|
||||
private
|
||||
|
||||
def make_file_request(drive_item_id, http, select_url_query)
|
||||
response = http.get(Util.join_uri_path(@storage.uri, uri_path_for(drive_item_id) + select_url_query))
|
||||
handle_responses(response)
|
||||
handle_response http.get("#{@storage.uri}#{uri_path_for(drive_item_id)}#{select_url_query}")
|
||||
end
|
||||
|
||||
def handle_responses(response)
|
||||
def handle_response(response)
|
||||
case response
|
||||
in { status: 200..299 }
|
||||
ServiceResult.success(result: response.json(symbolize_keys: true))
|
||||
@@ -71,16 +68,16 @@ module Storages
|
||||
ServiceResult.failure(result: :unauthorized,
|
||||
errors: Util.storage_error(response:, code: :unauthorized, source: self.class))
|
||||
else
|
||||
data = ::Storages::StorageErrorData.new(source: self.class, payload: response)
|
||||
ServiceResult.failure(result: :error, errors: ::Storages::StorageError.new(code: :error, data:))
|
||||
data = StorageErrorData.new(source: self.class, payload: response)
|
||||
ServiceResult.failure(result: :error, errors: StorageError.new(code: :error, data:))
|
||||
end
|
||||
end
|
||||
|
||||
def uri_path_for(file_id)
|
||||
if file_id == "/"
|
||||
"/v1.0/drives/#{@storage.drive_id}/root"
|
||||
"v1.0/drives/#{@storage.drive_id}/root"
|
||||
else
|
||||
"/v1.0/drives/#{@storage.drive_id}/items/#{file_id}"
|
||||
"v1.0/drives/#{@storage.drive_id}/items/#{file_id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+85
-73
@@ -28,86 +28,98 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Storages::Peripherals::StorageInteraction::OneDrive::Util
|
||||
using Storages::Peripherals::ServiceResultRefinements
|
||||
module Storages
|
||||
module Peripherals
|
||||
module StorageInteraction
|
||||
module OneDrive
|
||||
module Util
|
||||
using ServiceResultRefinements
|
||||
|
||||
class << self
|
||||
def mime_type(json)
|
||||
json.dig(:file, :mimeType) || (json.key?(:folder) ? "application/x-op-directory" : nil)
|
||||
end
|
||||
class << self
|
||||
def mime_type(json)
|
||||
json.dig(:file, :mimeType) || (json.key?(:folder) ? "application/x-op-directory" : nil)
|
||||
end
|
||||
|
||||
def using_user_token(storage, user, &)
|
||||
connection_manager = ::OAuthClients::ConnectionManager
|
||||
.new(user:, configuration: storage.oauth_configuration)
|
||||
def using_user_token(storage, user, &)
|
||||
connection_manager = ::OAuthClients::ConnectionManager
|
||||
.new(user:, configuration: storage.oauth_configuration)
|
||||
|
||||
connection_manager
|
||||
.get_access_token
|
||||
.match(
|
||||
on_success: ->(token) do
|
||||
connection_manager.request_with_token_refresh(token) { yield token }
|
||||
end,
|
||||
on_failure: ->(_) do
|
||||
ServiceResult.failure(
|
||||
result: :unauthorized,
|
||||
errors: ::Storages::StorageError.new(
|
||||
code: :unauthorized,
|
||||
data: ::Storages::StorageErrorData.new(source: connection_manager),
|
||||
log_message: "Query could not be created! No access token found!"
|
||||
connection_manager
|
||||
.get_access_token
|
||||
.match(
|
||||
on_success: ->(token) do
|
||||
connection_manager.request_with_token_refresh(token) { yield token }
|
||||
end,
|
||||
on_failure: ->(_) do
|
||||
ServiceResult.failure(
|
||||
result: :unauthorized,
|
||||
errors: StorageError.new(
|
||||
code: :unauthorized,
|
||||
data: StorageErrorData.new(source: connection_manager),
|
||||
log_message: "Query could not be created! No access token found!"
|
||||
)
|
||||
)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def storage_error(response:, code:, source:)
|
||||
data = StorageErrorData.new(source:, payload: response.json(symbolize_keys: true))
|
||||
|
||||
StorageError.new(code:, data:)
|
||||
end
|
||||
|
||||
def join_uri_path(uri, *)
|
||||
# We use `File.join` to ensure single `/` in between every part. This API will break if executed on a
|
||||
# Windows context, as it used `\` as file separators. But we anticipate that OpenProject
|
||||
# Server is not run on a Windows context.
|
||||
# URI::join cannot be used, as it behaves very different for the path parts depending on trailing slashes.
|
||||
File.join(uri.to_s, *)
|
||||
end
|
||||
|
||||
def json_content_type
|
||||
{ headers: { "Content-Type" => "application/json" } }
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def using_admin_token(storage)
|
||||
oauth_client = storage.oauth_configuration.basic_rack_oauth_client
|
||||
|
||||
token_result =
|
||||
begin
|
||||
Rails.cache.fetch("storage.#{storage.id}.access_token", expires_in: 50.minutes) do
|
||||
ServiceResult.success(result: oauth_client.access_token!(scope: "https://graph.microsoft.com/.default"))
|
||||
end
|
||||
rescue Rack::OAuth2::Client::Error => e
|
||||
ServiceResult.failure(errors: ::Storages::StorageError.new(
|
||||
code: :unauthorized,
|
||||
data: StorageErrorData.new(source: self.class),
|
||||
log_message: e.message
|
||||
))
|
||||
end
|
||||
|
||||
token_result.match(
|
||||
on_success: ->(token) do
|
||||
yield OpenProject.httpx.with(origin: storage.uri,
|
||||
headers: { authorization: "Bearer #{token.access_token}",
|
||||
accept: "application/json",
|
||||
"content-type": "application/json" })
|
||||
end,
|
||||
on_failure: ->(errors) { ServiceResult.failure(result: :unauthorized, errors:) }
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def extract_location(parent_reference, file_name = "")
|
||||
location = parent_reference[:path].gsub(/.*root:/, "")
|
||||
|
||||
appendix = file_name.blank? ? "" : "/#{file_name}"
|
||||
location.empty? ? "/#{file_name}" : "#{location}#{appendix}"
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def storage_error(response:, code:, source:)
|
||||
data = ::Storages::StorageErrorData.new(source:, payload: response.json(symbolize_keys: true))
|
||||
|
||||
::Storages::StorageError.new(code:, data:)
|
||||
end
|
||||
|
||||
def join_uri_path(uri, *)
|
||||
# We use `File.join` to ensure single `/` in between every part. This API will break if executed on a
|
||||
# Windows context, as it used `\` as file separators. But we anticipate that OpenProject
|
||||
# Server is not run on a Windows context.
|
||||
# URI::join cannot be used, as it behaves very different for the path parts depending on trailing slashes.
|
||||
File.join(uri.to_s, *)
|
||||
end
|
||||
|
||||
def json_content_type
|
||||
{ headers: { "Content-Type" => "application/json" } }
|
||||
end
|
||||
|
||||
def using_admin_token(storage)
|
||||
oauth_client = storage.oauth_configuration.basic_rack_oauth_client
|
||||
|
||||
token_result = begin
|
||||
Rails.cache.fetch("storage.#{storage.id}.access_token", expires_in: 50.minutes) do
|
||||
ServiceResult.success(result: oauth_client.access_token!(scope: "https://graph.microsoft.com/.default"))
|
||||
end
|
||||
rescue Rack::OAuth2::Client::Error => e
|
||||
ServiceResult.failure(errors: ::Storages::StorageError.new(
|
||||
code: :unauthorized,
|
||||
data: ::Storages::StorageErrorData.new(source: self.class),
|
||||
log_message: e.message
|
||||
))
|
||||
end
|
||||
|
||||
token_result.match(
|
||||
on_success: ->(token) do
|
||||
yield OpenProject.httpx.with(origin: storage.uri,
|
||||
headers: { authorization: "Bearer #{token.access_token}",
|
||||
accept: "application/json",
|
||||
"content-type": "application/json" })
|
||||
end,
|
||||
on_failure: ->(errors) { ServiceResult.failure(result: :unauthorized, errors:) }
|
||||
)
|
||||
end
|
||||
|
||||
def extract_location(parent_reference, file_name = "")
|
||||
location = parent_reference[:path].gsub(/.*root:/, "")
|
||||
|
||||
appendix = file_name.blank? ? "" : "/#{file_name}"
|
||||
location.empty? ? "/#{file_name}" : "#{location}#{appendix}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2024 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
|
||||
StorageFileId = Data.define(:id)
|
||||
end
|
||||
@@ -46,10 +46,10 @@ module Storages
|
||||
|
||||
def call
|
||||
source_file_links = FileLink
|
||||
.includes(:creator)
|
||||
.where(storage: @source.storage,
|
||||
container_id: @work_packages_map.keys,
|
||||
container_type: "WorkPackage")
|
||||
.includes(:creator)
|
||||
.where(storage: @source.storage,
|
||||
container_id: @work_packages_map.keys,
|
||||
container_type: "WorkPackage")
|
||||
|
||||
with_locale_for(@user) do
|
||||
if @source.project_folder_automatic?
|
||||
@@ -62,6 +62,7 @@ module Storages
|
||||
|
||||
private
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def create_managed_file_links(source_file_links)
|
||||
location_map = build_location_map(
|
||||
source_files_info(source_file_links).result,
|
||||
@@ -83,6 +84,9 @@ module Storages
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def build_location_map(source_files, target_location_map)
|
||||
# We need this due to inconsistencies of how we represent the File Path
|
||||
target_location_map.transform_keys! { |key| key.starts_with?("/") ? key : "/#{key}" }
|
||||
@@ -99,6 +103,8 @@ module Storages
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def auth_strategy
|
||||
Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken
|
||||
.strategy
|
||||
@@ -114,7 +120,7 @@ module Storages
|
||||
|
||||
def target_files_map
|
||||
Peripherals::Registry
|
||||
.resolve("#{@source.storage.short_provider_type}.queries.folder_files_file_ids_deep_query")
|
||||
.resolve("#{@source.storage.short_provider_type}.queries.folder_files_file_ids_deep")
|
||||
.call(storage: @source.storage, folder: Peripherals::ParentFolder.new(@target.project_folder_location))
|
||||
end
|
||||
|
||||
|
||||
+6
-6
@@ -31,7 +31,7 @@
|
||||
require "spec_helper"
|
||||
require_module_spec_helper
|
||||
|
||||
RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand, :vcr, :webmock do
|
||||
RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand, :webmock do
|
||||
let(:user) { create(:user) }
|
||||
let(:storage) do
|
||||
create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user)
|
||||
@@ -40,14 +40,14 @@ RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolde
|
||||
Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user)
|
||||
end
|
||||
|
||||
it_behaves_like "basic command setup"
|
||||
it_behaves_like "create_folder_command: basic command setup"
|
||||
|
||||
context "when creating a folder in the root", vcr: "nextcloud/create_folder_root" do
|
||||
let(:folder_name) { "Földer CreatedBy Çommand" }
|
||||
let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") }
|
||||
let(:path) { "/#{folder_name}" }
|
||||
|
||||
it_behaves_like "successful folder creation"
|
||||
it_behaves_like "create_folder_command: successful folder creation"
|
||||
end
|
||||
|
||||
context "when creating a folder in a parent folder", vcr: "nextcloud/create_folder_parent" do
|
||||
@@ -55,7 +55,7 @@ RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolde
|
||||
let(:parent_location) { Storages::Peripherals::ParentFolder.new("/Folder") }
|
||||
let(:path) { "/Folder/#{folder_name}" }
|
||||
|
||||
it_behaves_like "successful folder creation"
|
||||
it_behaves_like "create_folder_command: successful folder creation"
|
||||
end
|
||||
|
||||
context "when creating a folder in a non-existing parent folder", vcr: "nextcloud/create_folder_parent_not_found" do
|
||||
@@ -63,7 +63,7 @@ RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolde
|
||||
let(:parent_location) { Storages::Peripherals::ParentFolder.new("/DeathStar3") }
|
||||
let(:error_source) { described_class }
|
||||
|
||||
it_behaves_like "parent not found"
|
||||
it_behaves_like "create_folder_command: parent not found"
|
||||
end
|
||||
|
||||
context "when folder already exists", vcr: "nextcloud/create_folder_already_exists" do
|
||||
@@ -71,7 +71,7 @@ RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolde
|
||||
let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") }
|
||||
let(:error_source) { described_class }
|
||||
|
||||
it_behaves_like "folder already exists"
|
||||
it_behaves_like "create_folder_command: folder already exists"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2024 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::Peripherals::StorageInteraction::Nextcloud::FolderFilesFileIdsDeepQuery, :webmock do
|
||||
let(:user) { create(:user) }
|
||||
let(:storage) do
|
||||
create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user)
|
||||
end
|
||||
let(:auth_strategy) do
|
||||
Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user)
|
||||
end
|
||||
|
||||
it_behaves_like "folder_files_file_ids_deep_query: basic query setup"
|
||||
|
||||
context "with parent folder being root", vcr: "nextcloud/folder_files_file_ids_deep_query_root" do
|
||||
let(:folder) { Storages::Peripherals::ParentFolder.new("/") }
|
||||
let(:expected_ids) do
|
||||
{
|
||||
"/" => "2",
|
||||
"/Folder with spaces" => "165",
|
||||
"/Folder with spaces/New Requests" => "166",
|
||||
"/Folder with spaces/New Requests/request_001.md" => "167",
|
||||
"/Folder with spaces/New Requests/request_002.md" => "168",
|
||||
"/Folder" => "169",
|
||||
"/Folder/android-studio-2021.3.1.17-linux.tar.gz" => "267",
|
||||
"/Folder/empty" => "172",
|
||||
"/Folder/Ümlæûts" => "350",
|
||||
"/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "351",
|
||||
"/Practical_guide_to_BAGGM_Digital.pdf" => "295",
|
||||
"/Readme.md" => "268"
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like "folder_files_file_ids_deep_query: successful query"
|
||||
end
|
||||
|
||||
context "with a given parent folder", vcr: "nextcloud/folder_files_file_ids_deep_query_parent_folder" do
|
||||
let(:folder) { Storages::Peripherals::ParentFolder.new("/Folder") }
|
||||
let(:expected_ids) do
|
||||
{
|
||||
"/Folder" => "169",
|
||||
"/Folder/android-studio-2021.3.1.17-linux.tar.gz" => "267",
|
||||
"/Folder/empty" => "172",
|
||||
"/Folder/Ümlæûts" => "350",
|
||||
"/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "351"
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like "folder_files_file_ids_deep_query: successful query"
|
||||
end
|
||||
|
||||
context "with not existent parent folder", vcr: "nextcloud/folder_files_file_ids_deep_query_invalid_parent" do
|
||||
let(:folder) { Storages::Peripherals::ParentFolder.new("/I/just/made/that/up") }
|
||||
let(:error_source) { Storages::Peripherals::StorageInteraction::Nextcloud::Internal::PropfindQuery }
|
||||
|
||||
it_behaves_like "folder_files_file_ids_deep_query: not found"
|
||||
end
|
||||
end
|
||||
+6
-6
@@ -31,20 +31,20 @@
|
||||
require "spec_helper"
|
||||
require_module_spec_helper
|
||||
|
||||
RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::CreateFolderCommand, :vcr, :webmock do
|
||||
RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::CreateFolderCommand, :webmock do
|
||||
let(:storage) { create(:sharepoint_dev_drive_storage) }
|
||||
let(:auth_strategy) do
|
||||
Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials.strategy
|
||||
end
|
||||
|
||||
it_behaves_like "basic command setup"
|
||||
it_behaves_like "create_folder_command: basic command setup"
|
||||
|
||||
context "when creating a folder in the root", vcr: "one_drive/create_folder_root" do
|
||||
let(:folder_name) { "Földer CreatedBy Çommand" }
|
||||
let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") }
|
||||
let(:path) { "/#{folder_name}" }
|
||||
|
||||
it_behaves_like "successful folder creation"
|
||||
it_behaves_like "create_folder_command: successful folder creation"
|
||||
end
|
||||
|
||||
context "when creating a folder in a parent folder", vcr: "one_drive/create_folder_parent" do
|
||||
@@ -52,7 +52,7 @@ RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::CreateFolder
|
||||
let(:parent_location) { Storages::Peripherals::ParentFolder.new("01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU") }
|
||||
let(:path) { "/Folder with spaces/#{folder_name}" }
|
||||
|
||||
it_behaves_like "successful folder creation"
|
||||
it_behaves_like "create_folder_command: successful folder creation"
|
||||
end
|
||||
|
||||
context "when creating a folder in a non-existing parent folder", vcr: "one_drive/create_folder_parent_not_found" do
|
||||
@@ -60,7 +60,7 @@ RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::CreateFolder
|
||||
let(:parent_location) { Storages::Peripherals::ParentFolder.new("01AZJL5PKU2WV3U3RKKFF4A7ZCWVBXRTEU") }
|
||||
let(:error_source) { described_class }
|
||||
|
||||
it_behaves_like "parent not found"
|
||||
it_behaves_like "create_folder_command: parent not found"
|
||||
end
|
||||
|
||||
context "when folder already exists", vcr: "one_drive/create_folder_already_exists" do
|
||||
@@ -68,7 +68,7 @@ RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::CreateFolder
|
||||
let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") }
|
||||
let(:error_source) { described_class }
|
||||
|
||||
it_behaves_like "folder already exists"
|
||||
it_behaves_like "create_folder_command: folder already exists"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
+52
-112
@@ -32,126 +32,66 @@ require "spec_helper"
|
||||
require_module_spec_helper
|
||||
|
||||
RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::FolderFilesFileIdsDeepQuery, :webmock do
|
||||
using Storages::Peripherals::ServiceResultRefinements
|
||||
let(:storage) { create(:sharepoint_dev_drive_storage) }
|
||||
let(:auth_strategy) do
|
||||
Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials.strategy
|
||||
end
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) }
|
||||
let(:folder) { Storages::Peripherals::ParentFolder.new("/") }
|
||||
it_behaves_like "folder_files_file_ids_deep_query: basic query setup"
|
||||
|
||||
describe "#call" do
|
||||
it "responds with correct parameters" do
|
||||
expect(described_class).to respond_to(:call)
|
||||
|
||||
method = described_class.method(:call)
|
||||
expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq folder])
|
||||
context "with parent folder being root", vcr: "one_drive/folder_files_file_ids_deep_query_root" do
|
||||
let(:folder) { Storages::Peripherals::ParentFolder.new("/") }
|
||||
let(:expected_ids) do
|
||||
{
|
||||
"/" => "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ",
|
||||
"/Folder with spaces" => "01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU",
|
||||
"/Folder with spaces/very empty folder" => "01AZJL5PMGEIRPHZPHRRH2NM3D734VIR7H",
|
||||
"/Folder with spaces/wordle1.png" => "01AZJL5PPMSBBO3R2BIZHJFCELSW3RP7GN",
|
||||
"/Folder with spaces/wordle2.png" => "01AZJL5PIIFUD6A765KBAIAEMYACAFB2WP",
|
||||
"/Folder with spaces/wordle3.png" => "01AZJL5PL4AUJEU43CQZFJKN7BQPRP3BLF",
|
||||
"/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR",
|
||||
"/Folder/Images" => "01AZJL5PMIF7ND3KH6FVDLZYP3E36ERFGI",
|
||||
"/Folder/Subfolder" => "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG",
|
||||
"/Folder/Ümlæûts" => "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB",
|
||||
"/Folder/Document.docx" => "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU",
|
||||
"/Folder/Sheet.xlsx" => "01AZJL5PLB7SH7633RMBHIH6KVMQRU4RJS",
|
||||
"/Folder/Images/der_laufende.jpeg" => "01AZJL5PLZFCARRQIDFJF36UL2WTLXTNSY",
|
||||
"/Folder/Images/written_in_stone.webp" => "01AZJL5PLNCKWYI752YBHYYJ6RBFZWOZ46",
|
||||
"/Folder/Subfolder/NextcloudHub.md" => "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA",
|
||||
"/Folder/Subfolder/test.txt" => "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ",
|
||||
"/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE",
|
||||
"/Permissions Folder" => "01AZJL5PN3LVLHH2RSZZDJ6ZFAD3OWSGYB"
|
||||
}
|
||||
end
|
||||
|
||||
context "with outbound requests successful" do
|
||||
subject do
|
||||
described_class.call(storage:, folder:).result
|
||||
end
|
||||
it_behaves_like "folder_files_file_ids_deep_query: successful query"
|
||||
end
|
||||
|
||||
context "with parent folder being root", vcr: "one_drive/folder_files_file_ids_deep_query_root" do
|
||||
it "returns the file id dictionary" do
|
||||
expect(subject.transform_values(&:id))
|
||||
.to eq({
|
||||
"/" => "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ",
|
||||
"/Folder with spaces" => "01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU",
|
||||
"/Folder with spaces/very empty folder" => "01AZJL5PMGEIRPHZPHRRH2NM3D734VIR7H",
|
||||
"/Folder with spaces/wordle1.png" => "01AZJL5PPMSBBO3R2BIZHJFCELSW3RP7GN",
|
||||
"/Folder with spaces/wordle2.png" => "01AZJL5PIIFUD6A765KBAIAEMYACAFB2WP",
|
||||
"/Folder with spaces/wordle3.png" => "01AZJL5PL4AUJEU43CQZFJKN7BQPRP3BLF",
|
||||
"/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR",
|
||||
"/Folder/Images" => "01AZJL5PMIF7ND3KH6FVDLZYP3E36ERFGI",
|
||||
"/Folder/Subfolder" => "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG",
|
||||
"/Folder/Ümlæûts" => "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB",
|
||||
"/Folder/Document.docx" => "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU",
|
||||
"/Folder/Sheet.xlsx" => "01AZJL5PLB7SH7633RMBHIH6KVMQRU4RJS",
|
||||
"/Folder/Images/der_laufende.jpeg" => "01AZJL5PLZFCARRQIDFJF36UL2WTLXTNSY",
|
||||
"/Folder/Images/written_in_stone.webp" => "01AZJL5PLNCKWYI752YBHYYJ6RBFZWOZ46",
|
||||
"/Folder/Subfolder/NextcloudHub.md" => "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA",
|
||||
"/Folder/Subfolder/test.txt" => "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ",
|
||||
"/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE",
|
||||
"/Permissions Folder" => "01AZJL5PN3LVLHH2RSZZDJ6ZFAD3OWSGYB"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context "with a given parent folder", vcr: "one_drive/folder_files_file_ids_deep_query_parent_folder" do
|
||||
let(:folder) { Storages::Peripherals::ParentFolder.new("01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR") }
|
||||
|
||||
it "returns the file id dictionary" do
|
||||
expect(subject.transform_values(&:id))
|
||||
.to eq({
|
||||
"/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR",
|
||||
"/Folder/Images" => "01AZJL5PMIF7ND3KH6FVDLZYP3E36ERFGI",
|
||||
"/Folder/Subfolder" => "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG",
|
||||
"/Folder/Ümlæûts" => "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB",
|
||||
"/Folder/Document.docx" => "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU",
|
||||
"/Folder/Sheet.xlsx" => "01AZJL5PLB7SH7633RMBHIH6KVMQRU4RJS",
|
||||
"/Folder/Images/der_laufende.jpeg" => "01AZJL5PLZFCARRQIDFJF36UL2WTLXTNSY",
|
||||
"/Folder/Images/written_in_stone.webp" => "01AZJL5PLNCKWYI752YBHYYJ6RBFZWOZ46",
|
||||
"/Folder/Subfolder/NextcloudHub.md" => "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA",
|
||||
"/Folder/Subfolder/test.txt" => "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ",
|
||||
"/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE"
|
||||
})
|
||||
end
|
||||
end
|
||||
context "with a given parent folder", vcr: "one_drive/folder_files_file_ids_deep_query_parent_folder" do
|
||||
let(:folder) { Storages::Peripherals::ParentFolder.new("01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR") }
|
||||
let(:expected_ids) do
|
||||
{
|
||||
"/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR",
|
||||
"/Folder/Images" => "01AZJL5PMIF7ND3KH6FVDLZYP3E36ERFGI",
|
||||
"/Folder/Subfolder" => "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG",
|
||||
"/Folder/Ümlæûts" => "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB",
|
||||
"/Folder/Document.docx" => "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU",
|
||||
"/Folder/Sheet.xlsx" => "01AZJL5PLB7SH7633RMBHIH6KVMQRU4RJS",
|
||||
"/Folder/Images/der_laufende.jpeg" => "01AZJL5PLZFCARRQIDFJF36UL2WTLXTNSY",
|
||||
"/Folder/Images/written_in_stone.webp" => "01AZJL5PLNCKWYI752YBHYYJ6RBFZWOZ46",
|
||||
"/Folder/Subfolder/NextcloudHub.md" => "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA",
|
||||
"/Folder/Subfolder/test.txt" => "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ",
|
||||
"/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE"
|
||||
}
|
||||
end
|
||||
|
||||
context "with not existent parent folder", vcr: "one_drive/folder_files_file_ids_deep_query_invalid_parent" do
|
||||
let(:folder) { Storages::Peripherals::ParentFolder.new("/I/just/made/that/up") }
|
||||
it_behaves_like "folder_files_file_ids_deep_query: successful query"
|
||||
end
|
||||
|
||||
it "must return not found" do
|
||||
result = described_class.call(storage:, folder:)
|
||||
expect(result).to be_failure
|
||||
expect(result.error_source).to be_a(described_class)
|
||||
context "with not existent parent folder", vcr: "one_drive/folder_files_file_ids_deep_query_invalid_parent" do
|
||||
let(:folder) { Storages::Peripherals::ParentFolder.new("/I/just/made/that/up") }
|
||||
let(:error_source) { Storages::Peripherals::StorageInteraction::OneDrive::Internal::DriveItemQuery }
|
||||
|
||||
result.match(
|
||||
on_failure: ->(error) { expect(error.code).to eq(:not_found) },
|
||||
on_success: ->(file_infos) { fail "Expected failure, got #{file_infos}" }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "with invalid oauth credentials", vcr: "one_drive/folder_files_file_ids_deep_query_invalid_credentials" do
|
||||
before do
|
||||
unauthorized_http = OpenProject.httpx.with(origin: storage.uri,
|
||||
headers: { authorization: "Bearer YouShallNotPass",
|
||||
accept: "application/json",
|
||||
"content-type": "application/json" })
|
||||
allow(Storages::Peripherals::StorageInteraction::OneDrive::Util)
|
||||
.to receive(:using_admin_token)
|
||||
.and_yield(unauthorized_http)
|
||||
end
|
||||
|
||||
it "must return unauthorized" do
|
||||
result = described_class.call(storage:, folder:)
|
||||
expect(result).to be_failure
|
||||
expect(result.error_source).to be_a(described_class)
|
||||
|
||||
result.match(
|
||||
on_failure: ->(error) { expect(error.code).to eq(:unauthorized) },
|
||||
on_success: ->(file_infos) { fail "Expected failure, got #{file_infos}" }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "with network errors" do
|
||||
before do
|
||||
request = HTTPX::Request.new(:get, "https://my.timeout.org/")
|
||||
httpx_double = class_double(HTTPX, get: HTTPX::ErrorResponse.new(request, "Timeout happens", {}))
|
||||
allow(Storages::Peripherals::StorageInteraction::OneDrive::Util)
|
||||
.to receive(:using_admin_token)
|
||||
.and_yield(httpx_double)
|
||||
end
|
||||
|
||||
it "must return an error with wrapped network error response" do
|
||||
error = described_class.call(storage:, folder:)
|
||||
expect(error).to be_failure
|
||||
expect(error.result).to eq(:error)
|
||||
expect(error.error_payload).to be_a(HTTPX::ErrorResponse)
|
||||
end
|
||||
end
|
||||
it_behaves_like "folder_files_file_ids_deep_query: not found"
|
||||
end
|
||||
end
|
||||
|
||||
+1
-1
@@ -674,7 +674,7 @@ RSpec.describe Storages::NextcloudGroupFolderPropertiesSyncService, :webmock do
|
||||
expect(OpenProject.logger)
|
||||
.to have_received(:warn)
|
||||
.with(folder: "OpenProject",
|
||||
command: Storages::Peripherals::StorageInteraction::Nextcloud::Internal::PropfindQuery,
|
||||
command: Storages::Peripherals::StorageInteraction::Nextcloud::Internal::PropfindQueryLegacy,
|
||||
message: "Outbound request destination not found",
|
||||
data: { status: 404, body: "" })
|
||||
end
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: propfind
|
||||
uri: https://nextcloud.local/remote.php/dav/files/admin/I/just/made/that/up
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
<?xml version="1.0"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
|
||||
<d:prop>
|
||||
<oc:fileid/>
|
||||
<nc:acl-list/>
|
||||
</d:prop>
|
||||
</d:propfind>
|
||||
headers:
|
||||
Depth:
|
||||
- infinity
|
||||
Authorization:
|
||||
- Bearer <BEARER TOKEN>
|
||||
User-Agent:
|
||||
- httpx.rb/1.2.4
|
||||
Accept:
|
||||
- "*/*"
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Content-Type:
|
||||
- application/xml; charset=utf-8
|
||||
Content-Length:
|
||||
- '192'
|
||||
response:
|
||||
status:
|
||||
code: 404
|
||||
message: Not Found
|
||||
headers:
|
||||
Cache-Control:
|
||||
- no-store, no-cache, must-revalidate
|
||||
Content-Security-Policy:
|
||||
- default-src 'none';
|
||||
Content-Type:
|
||||
- application/xml; charset=utf-8
|
||||
Date:
|
||||
- Mon, 29 Apr 2024 09:27:02 GMT
|
||||
Dav:
|
||||
- 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search,
|
||||
nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar
|
||||
Expires:
|
||||
- Thu, 19 Nov 1981 08:52:00 GMT
|
||||
Pragma:
|
||||
- no-cache
|
||||
Referrer-Policy:
|
||||
- no-referrer
|
||||
Server:
|
||||
- Apache/2.4.59 (Debian)
|
||||
Set-Cookie:
|
||||
- oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd; path=/; secure; HttpOnly; SameSite=Lax,
|
||||
oc_sessionPassphrase=eLXRRbs1vLi5xkH0WJYyVwQJqpjGro0c0Zg3nUyYr5VYgUqF9Bt2M8oZ9gNzG3r%2BetStTAiHiIzJoK9h3r4uj41hg%2Bp0rmSAbfcYTXntvL8N3gQIW%2BkkeMHfhya3npEJ;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true;
|
||||
path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax,
|
||||
__Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri,
|
||||
31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=d5add640a32a32d41d02170d73dd30bd;
|
||||
path=/; secure; HttpOnly; SameSite=Lax
|
||||
Vary:
|
||||
- Brief,Prefer
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Permitted-Cross-Domain-Policies:
|
||||
- none
|
||||
X-Powered-By:
|
||||
- PHP/8.2.18
|
||||
X-Robots-Tag:
|
||||
- noindex, nofollow
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block
|
||||
Content-Length:
|
||||
- '231'
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
|
||||
<s:exception>Sabre\DAV\Exception\NotFound</s:exception>
|
||||
<s:message>File with name //I could not be located</s:message>
|
||||
</d:error>
|
||||
recorded_at: Mon, 29 Apr 2024 09:27:02 GMT
|
||||
recorded_with: VCR 6.2.0
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: propfind
|
||||
uri: https://nextcloud.local/remote.php/dav/files/admin/Folder
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
<?xml version="1.0"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
|
||||
<d:prop>
|
||||
<oc:fileid/>
|
||||
<nc:acl-list/>
|
||||
</d:prop>
|
||||
</d:propfind>
|
||||
headers:
|
||||
Depth:
|
||||
- infinity
|
||||
Authorization:
|
||||
- Bearer <BEARER TOKEN>
|
||||
User-Agent:
|
||||
- httpx.rb/1.2.4
|
||||
Accept:
|
||||
- "*/*"
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Content-Type:
|
||||
- application/xml; charset=utf-8
|
||||
Content-Length:
|
||||
- '192'
|
||||
response:
|
||||
status:
|
||||
code: 207
|
||||
message: Multi-Status
|
||||
headers:
|
||||
Cache-Control:
|
||||
- no-store, no-cache, must-revalidate
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Security-Policy:
|
||||
- default-src 'none';
|
||||
Content-Type:
|
||||
- application/xml; charset=utf-8
|
||||
Date:
|
||||
- Mon, 29 Apr 2024 09:27:02 GMT
|
||||
Dav:
|
||||
- 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search,
|
||||
nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar
|
||||
Expires:
|
||||
- Thu, 19 Nov 1981 08:52:00 GMT
|
||||
Pragma:
|
||||
- no-cache
|
||||
Referrer-Policy:
|
||||
- no-referrer
|
||||
Server:
|
||||
- Apache/2.4.59 (Debian)
|
||||
Set-Cookie:
|
||||
- oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9; path=/; secure; HttpOnly; SameSite=Lax,
|
||||
oc_sessionPassphrase=QGRWLsaUpaf5JsdI0cV162JoZvcVfhQVV61E6mpPh%2BvmQRlBwBWC%2BP42vMFWwOmkkWTTF9miI%2FBEoiqthA3UQ3ib0UiO40kKObozzqQBv5d%2FsnkiMF0pJJMnsdM8JWTT;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true;
|
||||
path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax,
|
||||
__Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri,
|
||||
31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=bbc1b8b5d96bfb844ecb9f000d05ccf9;
|
||||
path=/; secure; HttpOnly; SameSite=Lax
|
||||
Vary:
|
||||
- Brief,Prefer
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Debug-Token:
|
||||
- C2A5yYZ2GjWsVq5rexEF
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Permitted-Cross-Domain-Policies:
|
||||
- none
|
||||
X-Powered-By:
|
||||
- PHP/8.2.18
|
||||
X-Request-Id:
|
||||
- C2A5yYZ2GjWsVq5rexEF
|
||||
X-Robots-Tag:
|
||||
- noindex, nofollow
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block
|
||||
Content-Length:
|
||||
- '366'
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"><d:response><d:href>/remote.php/dav/files/admin/Folder/</d:href><d:propstat><d:prop><oc:fileid>169</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Folder/android-studio-2021.3.1.17-linux.tar.gz</d:href><d:propstat><d:prop><oc:fileid>267</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Folder/empty/</d:href><d:propstat><d:prop><oc:fileid>172</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Folder/%c3%9cml%c3%a6%c3%bbts/</d:href><d:propstat><d:prop><oc:fileid>350</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Folder/%c3%9cml%c3%a6%c3%bbts/Anr%c3%bcchiges%20deutsches%20Dokument.docx</d:href><d:propstat><d:prop><oc:fileid>351</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response></d:multistatus>
|
||||
recorded_at: Mon, 29 Apr 2024 09:27:02 GMT
|
||||
recorded_with: VCR 6.2.0
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: propfind
|
||||
uri: https://nextcloud.local/remote.php/dav/files/admin/
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
<?xml version="1.0"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
|
||||
<d:prop>
|
||||
<oc:fileid/>
|
||||
<nc:acl-list/>
|
||||
</d:prop>
|
||||
</d:propfind>
|
||||
headers:
|
||||
Depth:
|
||||
- infinity
|
||||
Authorization:
|
||||
- Bearer <BEARER TOKEN>
|
||||
User-Agent:
|
||||
- httpx.rb/1.2.4
|
||||
Accept:
|
||||
- "*/*"
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Content-Type:
|
||||
- application/xml; charset=utf-8
|
||||
Content-Length:
|
||||
- '192'
|
||||
response:
|
||||
status:
|
||||
code: 207
|
||||
message: Multi-Status
|
||||
headers:
|
||||
Cache-Control:
|
||||
- no-store, no-cache, must-revalidate
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Security-Policy:
|
||||
- default-src 'none';
|
||||
Content-Type:
|
||||
- application/xml; charset=utf-8
|
||||
Date:
|
||||
- Mon, 29 Apr 2024 09:21:37 GMT
|
||||
Dav:
|
||||
- 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search,
|
||||
nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar
|
||||
Expires:
|
||||
- Thu, 19 Nov 1981 08:52:00 GMT
|
||||
Pragma:
|
||||
- no-cache
|
||||
Referrer-Policy:
|
||||
- no-referrer
|
||||
Server:
|
||||
- Apache/2.4.59 (Debian)
|
||||
Set-Cookie:
|
||||
- oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2; path=/; secure; HttpOnly; SameSite=Lax,
|
||||
oc_sessionPassphrase=qHbGrRqzApLz65yOgbjo0F50qZOP7tl5seLjGa5MF5xAsfWUBS86U4TbnP6wBuwrqH1oAgAR19gOq6FYJvymPZW3tkMSKR9kS5YaIuyIWuqjif2txirDkqaymU7cOOVG;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true;
|
||||
path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax,
|
||||
__Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri,
|
||||
31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2;
|
||||
path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dce80de6ee27126cc7f063c499bbf2d2;
|
||||
path=/; secure; HttpOnly; SameSite=Lax
|
||||
Vary:
|
||||
- Brief,Prefer
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Debug-Token:
|
||||
- 6Cb1TBf2Ullw34vWD3yf
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Permitted-Cross-Domain-Policies:
|
||||
- none
|
||||
X-Powered-By:
|
||||
- PHP/8.2.18
|
||||
X-Request-Id:
|
||||
- 6Cb1TBf2Ullw34vWD3yf
|
||||
X-Robots-Tag:
|
||||
- noindex, nofollow
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block
|
||||
Content-Length:
|
||||
- '477'
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |
|
||||
<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"><d:response><d:href>/remote.php/dav/files/admin/</d:href><d:propstat><d:prop><oc:fileid>2</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Folder/</d:href><d:propstat><d:prop><oc:fileid>169</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Folder/android-studio-2021.3.1.17-linux.tar.gz</d:href><d:propstat><d:prop><oc:fileid>267</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Folder/empty/</d:href><d:propstat><d:prop><oc:fileid>172</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Folder/%c3%9cml%c3%a6%c3%bbts/</d:href><d:propstat><d:prop><oc:fileid>350</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Folder/%c3%9cml%c3%a6%c3%bbts/Anr%c3%bcchiges%20deutsches%20Dokument.docx</d:href><d:propstat><d:prop><oc:fileid>351</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Folder%20with%20spaces/</d:href><d:propstat><d:prop><oc:fileid>165</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Folder%20with%20spaces/New%20Requests/</d:href><d:propstat><d:prop><oc:fileid>166</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Folder%20with%20spaces/New%20Requests/request_001.md</d:href><d:propstat><d:prop><oc:fileid>167</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Folder%20with%20spaces/New%20Requests/request_002.md</d:href><d:propstat><d:prop><oc:fileid>168</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Practical_guide_to_BAGGM_Digital.pdf</d:href><d:propstat><d:prop><oc:fileid>295</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response><d:response><d:href>/remote.php/dav/files/admin/Readme.md</d:href><d:propstat><d:prop><oc:fileid>268</oc:fileid></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat><d:propstat><d:prop><nc:acl-list/></d:prop><d:status>HTTP/1.1 404 Not Found</d:status></d:propstat></d:response></d:multistatus>
|
||||
recorded_at: Mon, 29 Apr 2024 09:21:38 GMT
|
||||
recorded_with: VCR 6.2.0
|
||||
+4
-4
@@ -28,7 +28,7 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
RSpec.shared_examples_for "basic command setup" do
|
||||
RSpec.shared_examples_for "create_folder_command: basic command setup" do
|
||||
it "is registered as commands.create_folder" do
|
||||
expect(Storages::Peripherals::Registry
|
||||
.resolve("#{storage.short_provider_type}.commands.create_folder")).to eq(described_class)
|
||||
@@ -45,7 +45,7 @@ RSpec.shared_examples_for "basic command setup" do
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples_for "successful folder creation" do
|
||||
RSpec.shared_examples_for "create_folder_command: successful folder creation" do
|
||||
it "creates a folder" do
|
||||
result = described_class.call(storage:, auth_strategy:, folder_name:, parent_location:)
|
||||
|
||||
@@ -60,7 +60,7 @@ RSpec.shared_examples_for "successful folder creation" do
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples_for "parent not found" do
|
||||
RSpec.shared_examples_for "create_folder_command: parent not found" do
|
||||
it "returns a failure" do
|
||||
result = described_class.call(storage:, auth_strategy:, folder_name:, parent_location:)
|
||||
|
||||
@@ -72,7 +72,7 @@ RSpec.shared_examples_for "parent not found" do
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples_for "folder already exists" do
|
||||
RSpec.shared_examples_for "create_folder_command: folder already exists" do
|
||||
it "returns a failure" do
|
||||
result = described_class.call(storage:, auth_strategy:, folder_name:, parent_location:)
|
||||
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2024 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.
|
||||
#++
|
||||
|
||||
RSpec.shared_examples_for "folder_files_file_ids_deep_query: basic query setup" do
|
||||
it "is registered as queries.folder_files_file_ids_deep" do
|
||||
expect(Storages::Peripherals::Registry
|
||||
.resolve("#{storage.short_provider_type}.queries.folder_files_file_ids_deep")).to eq(described_class)
|
||||
end
|
||||
|
||||
it "responds to #call with correct parameters" do
|
||||
expect(described_class).to respond_to(:call)
|
||||
|
||||
method = described_class.method(:call)
|
||||
expect(method.parameters).to contain_exactly(%i[keyreq storage],
|
||||
%i[keyreq auth_strategy],
|
||||
%i[keyreq folder])
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples_for "folder_files_file_ids_deep_query: successful query" do
|
||||
it "returns a map of locations to file ids" do
|
||||
result = described_class.call(storage:, auth_strategy:, folder:)
|
||||
|
||||
expect(result).to be_success
|
||||
|
||||
response = result.result
|
||||
expect(response.transform_values(&:id)).to eq(expected_ids)
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples_for "folder_files_file_ids_deep_query: not found" do
|
||||
it "returns a failure" do
|
||||
result = described_class.call(storage:, auth_strategy:, folder:)
|
||||
|
||||
expect(result).to be_failure
|
||||
|
||||
error = result.errors
|
||||
expect(error.code).to eq(:not_found)
|
||||
expect(error.data.source).to eq(error_source)
|
||||
end
|
||||
end
|
||||
@@ -123,7 +123,7 @@ RSpec.describe Storages::CopyProjectFoldersJob, :job, :webmock, with_good_job_ba
|
||||
describe "managed project folders" do
|
||||
before do
|
||||
Storages::Peripherals::Registry
|
||||
.stub("#{storage.short_provider_type}.queries.folder_files_file_ids_deep_query", ->(storage:, folder:) {
|
||||
.stub("#{storage.short_provider_type}.queries.folder_files_file_ids_deep", ->(storage:, folder:) {
|
||||
ServiceResult.success(result: target_deep_file_ids)
|
||||
})
|
||||
|
||||
@@ -177,7 +177,7 @@ RSpec.describe Storages::CopyProjectFoldersJob, :job, :webmock, with_good_job_ba
|
||||
})
|
||||
|
||||
Storages::Peripherals::Registry
|
||||
.stub("#{storage.short_provider_type}.queries.folder_files_file_ids_deep_query", ->(storage:, folder:) {
|
||||
.stub("#{storage.short_provider_type}.queries.folder_files_file_ids_deep", ->(storage:, folder:) {
|
||||
ServiceResult.success(result: target_deep_file_ids)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user