Unify storage_file methods implemented in different places

This was highly duplicated code with incomplete and inconsistent
implementations across the board. Every implementation had own attributes
to add, that another implementation didn't care filling out and some
aspects, such as the escaping of the location were not consistent at all.
This commit is contained in:
Jan Sandbrink
2025-11-19 09:49:23 +01:00
parent 8f78589769
commit ae5362dc19
6 changed files with 134 additions and 193 deletions
@@ -55,9 +55,9 @@ module Storages
def create_folder_request(auth_strategy, request_url, path_prefix)
Authentication[auth_strategy].call(storage: @storage) do |http|
handle_response(http.mkcol(request_url)).bind do
handle_response(http.propfind(request_url, requested_properties)).bind do |response|
handle_response(http.propfind(request_url, storage_file_transformer.requested_properties)).bind do |response|
info "Folder successfully created"
storage_file(path_prefix, response)
storage_file_transformer.transform_document(response.xml, path_prefix)
end
end
end
@@ -80,43 +80,8 @@ module Storages
end
end
def requested_properties
Nokogiri::XML::Builder.new do |xml|
xml["d"].propfind("xmlns:d" => "DAV:", "xmlns:oc" => "http://owncloud.org/ns") do
xml["d"].prop do
xml["oc"].fileid
xml["oc"].size
xml["d"].getlastmodified
xml["oc"].permissions
xml["oc"].send(:"owner-display-name")
end
end
end.to_xml
end
# FIXME: Move this to a transformer?
# rubocop:disable Metrics/AbcSize
def storage_file(path_prefix, response)
xml = response.xml
path = xml.xpath("//d:response/d:href/text()").to_s
timestamp = xml.xpath("//d:response/d:propstat/d:prop/d:getlastmodified/text()").to_s
creator = xml.xpath("//d:response/d:propstat/d:prop/oc:owner-display-name/text()").to_s
location = CGI.unescapeURIComponent(
UrlBuilder.path(CGI.unescapeURIComponent(path)).gsub(path_prefix, "")
).delete_suffix("/")
Results::StorageFile.build(
id: xml.xpath("//d:response/d:propstat/d:prop/oc:fileid/text()").to_s,
name: location.split("/").last,
size: xml.xpath("//d:response/d:propstat/d:prop/oc:size/text()").to_s,
mime_type: "application/x-op-directory",
created_at: Time.zone.parse(timestamp),
last_modified_at: Time.zone.parse(timestamp),
created_by_name: creator,
last_modified_by_name: creator,
location:
)
# rubocop:enable Metrics/AbcSize
def storage_file_transformer
@storage_file_transformer ||= StorageFileTransformer.new
end
end
end
@@ -54,9 +54,9 @@ module Storages
Authentication[auth_strategy].call(storage: @storage) do |http|
handle_response(http.put(request_url, body: io)).bind do
info "File successfully uploaded, fetching its file info back..."
handle_response(http.propfind(request_url, requested_properties)).bind do |response|
handle_response(http.propfind(request_url, storage_file_transformer.requested_properties)).bind do |response|
info "Info of uploaded file fetched"
storage_file(path_prefix, response)
storage_file_transformer.transform_document(response.xml, path_prefix)
end
end
end
@@ -79,47 +79,9 @@ module Storages
end
end
# TODO: deduplicate
def requested_properties
Nokogiri::XML::Builder.new do |xml|
xml["d"].propfind("xmlns:d" => "DAV:", "xmlns:oc" => "http://owncloud.org/ns") do
xml["d"].prop do
xml["oc"].fileid
xml["oc"].size
xml["d"].getcontenttype
xml["d"].getlastmodified
xml["oc"].permissions
xml["oc"].send(:"owner-display-name")
end
end
end.to_xml
def storage_file_transformer
@storage_file_transformer ||= StorageFileTransformer.new
end
# TODO: deduplicate
# FIXME: Move this to a transformer?
# rubocop:disable Metrics/AbcSize
def storage_file(path_prefix, response)
xml = response.xml
path = xml.xpath("//d:response/d:href/text()").to_s
timestamp = xml.xpath("//d:response/d:propstat/d:prop/d:getlastmodified/text()").to_s
creator = xml.xpath("//d:response/d:propstat/d:prop/oc:owner-display-name/text()").to_s
location = CGI.unescapeURIComponent(
UrlBuilder.path(CGI.unescapeURIComponent(path)).gsub(path_prefix, "")
).delete_suffix("/")
Results::StorageFile.build(
id: xml.xpath("//d:response/d:propstat/d:prop/oc:fileid/text()").to_s,
name: location.split("/").last,
size: xml.xpath("//d:response/d:propstat/d:prop/oc:size/text()").to_s,
mime_type: xml.xpath("//d:response/d:propstat/d:prop/d:getcontenttype/text()").to_s,
created_at: Time.zone.parse(timestamp),
last_modified_at: Time.zone.parse(timestamp),
created_by_name: creator,
last_modified_by_name: creator,
location:
)
end
# rubocop:enable Metrics/AbcSize
end
end
end
@@ -36,7 +36,7 @@ module Storages
class FilesQuery < Base
def call(auth_strategy:, input_data:)
origin_user_id(auth_strategy:).bind do |origin_user|
@location_prefix = CGI.unescape(UrlBuilder.path(@storage.uri.path, "remote.php/dav/files", origin_user))
@location_prefix = UrlBuilder.path(@storage.uri.path, "remote.php/dav/files", origin_user)
make_request(auth_strategy:, folder: input_data.folder, origin_user:).bind do |xml|
storage_files(xml)
end
@@ -52,7 +52,7 @@ module Storages
"remote.php/dav/files",
origin_user,
folder.path),
xml: requested_properties)
xml: storage_file_transformer.requested_properties)
handle_response(response)
end
end
@@ -72,25 +72,10 @@ module Storages
end
end
# rubocop:disable Metrics/AbcSize
def requested_properties
Nokogiri::XML::Builder.new do |xml|
xml["d"].propfind("xmlns:d" => "DAV:", "xmlns:oc" => "http://owncloud.org/ns") do
xml["d"].prop do
xml["oc"].fileid
xml["oc"].size
xml["d"].getcontenttype
xml["d"].getlastmodified
xml["oc"].permissions
xml["oc"].send(:"owner-display-name")
end
end
end.to_xml
end
# rubocop:enable Metrics/AbcSize
def storage_files(xml)
parent, *files = xml.xpath("//d:response").to_a.map { |file_element| storage_file(file_element) }
parent, *files = xml.xpath("//d:response").to_a.map do |file_element|
storage_file_transformer.transform_element(file_element, @location_prefix).value!
end
Results::StorageFileCollection.build(files:, parent:, ancestors: ancestors(parent.location))
end
@@ -117,88 +102,8 @@ module Storages
location == "/" ? "Root" : CGI.unescape(location.split("/").last)
end
def storage_file(file_element)
location = location(file_element)
Results::StorageFile.new(
id: id(file_element),
name: name(location),
size: size(file_element),
mime_type: mime_type(file_element),
last_modified_at: last_modified_at(file_element),
created_by_name: created_by(file_element),
location:,
permissions: permissions(file_element)
)
end
def id(element)
element
.xpath(".//oc:fileid")
.map(&:inner_text)
.reject(&:empty?)
.first
end
def location(element)
texts = element
.xpath("d:href")
.map(&:inner_text)
return nil if texts.empty?
element_name = texts.first.delete_prefix(@location_prefix)
return element_name if element_name == "/"
element_name.delete_suffix("/")
end
def size(element)
element
.xpath(".//oc:size")
.map(&:inner_text)
.map { |e| Integer(e) }
.first
end
def mime_type(element)
element
.xpath(".//d:getcontenttype")
.map(&:inner_text)
.reject(&:empty?)
.first || "application/x-op-directory"
end
def last_modified_at(element)
element
.xpath(".//d:getlastmodified")
.map { |e| DateTime.parse(e) }
.first
end
def created_by(element)
element
.xpath(".//oc:owner-display-name")
.map(&:inner_text)
.reject(&:empty?)
.first
end
def permissions(element)
permissions_string =
element
.xpath(".//oc:permissions")
.map(&:inner_text)
.reject(&:empty?)
.first
# Nextcloud Dav permissions:
# https://github.com/nextcloud/server/blob/66648011c6bc278ace57230db44fd6d63d67b864/lib/public/Files/DavUtil.php
result = []
result << :readable if permissions_string&.include?("G")
result << :writeable if permissions_string&.match?(/W|CK/)
result
def storage_file_transformer
@storage_file_transformer ||= StorageFileTransformer.new
end
end
end
@@ -0,0 +1,108 @@
# 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
module Adapters
module Providers
module Nextcloud
class StorageFileTransformer
def transform_document(xml, path_prefix)
transform_element(xml.xpath("//d:response"), path_prefix)
end
def transform_element(xml, path_prefix)
location = extract_location(xml, path_prefix)
Results::StorageFile.build(
id: prop_text(xml, "oc:fileid"),
name: location == "/" ? "Root" : location.split("/").last,
size: extract_size(xml),
mime_type: prop_text(xml, "d:getcontenttype").presence || "application/x-op-directory",
last_modified_at: Time.zone.parse(prop_text(xml, "d:getlastmodified")),
created_by_name: prop_text(xml, "oc:owner-display-name").presence || "Unknown",
location:,
permissions: parse_permissions(prop_text(xml, "oc:permissions"))
)
end
# Returns the XML definition that needs to be sent to Nextcloud, so that it will respond with the required properties
# for a successful call to #transform.
# rubocop:disable Metrics/AbcSize
def requested_properties
Nokogiri::XML::Builder.new do |xml|
xml["d"].propfind("xmlns:d" => "DAV:", "xmlns:oc" => "http://owncloud.org/ns") do
xml["d"].prop do
xml["oc"].fileid
xml["oc"].size
xml["d"].getcontenttype
xml["d"].getlastmodified
xml["oc"].permissions
xml["oc"].send(:"owner-display-name")
end
end
end.to_xml
end
# rubocop:enable Metrics/AbcSize
private
def prop_text(xml, prop_key)
xml.xpath("./d:propstat/d:prop/#{prop_key}/text()").to_s
end
def extract_location(xml, path_prefix)
path = xml.xpath("./d:href/text()").to_s
location = CGI.unescapeURIComponent(UrlBuilder.path(CGI.unescapeURIComponent(path)).delete_prefix(path_prefix))
return "/" if location == ""
location
end
def extract_size(xml)
string = prop_text(xml, "oc:size")
return nil if string.blank?
Integer(string)
end
def parse_permissions(permissions_string)
# Nextcloud Dav permissions:
# https://github.com/nextcloud/server/blob/66648011c6bc278ace57230db44fd6d63d67b864/lib/public/Files/DavUtil.php
result = []
result << :readable if permissions_string&.include?("G")
result << :writeable if permissions_string&.match?(/W|CK/)
result
end
end
end
end
end
end
@@ -74,7 +74,7 @@ module Storages
last_modified_at: Time.zone.parse("2024-08-09T11:52:09Z"),
created_by_name: "Mara Jade",
last_modified_by_name: nil,
location: "/Folder%20with%20spaces",
location: "/Folder with spaces",
permissions: %i[readable writeable]),
Results::StorageFile.new(id: "562",
name: "Ümlæûts",
@@ -84,7 +84,7 @@ module Storages
last_modified_at: Time.zone.parse("2024-08-09T11:51:48Z"),
created_by_name: "Mara Jade",
last_modified_by_name: nil,
location: "/%c3%9cml%c3%a6%c3%bbts",
location: "/Ümlæûts",
permissions: %i[readable writeable])
],
parent: Results::StorageFile.new(id: "385",
@@ -117,7 +117,7 @@ module Storages
last_modified_at: Time.zone.parse("2024-08-09T11:53:24Z"),
created_by_name: "Mara Jade",
last_modified_by_name: nil,
location: "/Folder/Nested%20Folder/giphy.gif",
location: "/Folder/Nested Folder/giphy.gif",
permissions: %i[readable writeable]),
Results::StorageFile.new(id: "604",
name: "release_meme.jpg",
@@ -127,7 +127,7 @@ module Storages
last_modified_at: Time.zone.parse("2024-08-09T11:53:30Z"),
created_by_name: "Mara Jade",
last_modified_by_name: nil,
location: "/Folder/Nested%20Folder/release_meme.jpg",
location: "/Folder/Nested Folder/release_meme.jpg",
permissions: %i[readable writeable]),
Results::StorageFile.new(id: "602",
name: "todo.txt",
@@ -137,7 +137,7 @@ module Storages
last_modified_at: Time.zone.parse("2024-08-09T11:53:35Z"),
created_by_name: "Mara Jade",
last_modified_by_name: nil,
location: "/Folder/Nested%20Folder/todo.txt",
location: "/Folder/Nested Folder/todo.txt",
permissions: %i[readable writeable])
],
parent: Results::StorageFile.new(id: "601",
@@ -148,7 +148,7 @@ module Storages
last_modified_at: Time.zone.parse("2024-08-09T11:53:42Z"),
created_by_name: "Mara Jade",
last_modified_by_name: nil,
location: "/Folder/Nested%20Folder",
location: "/Folder/Nested Folder",
permissions: %i[readable writeable]),
ancestors: [
Results::StorageFileAncestor.new(name: "Root", location: "/"),
@@ -173,7 +173,7 @@ module Storages
last_modified_at: Time.zone.parse("2024-08-09T11:52:04Z"),
created_by_name: "Mara Jade",
last_modified_by_name: nil,
location: "/Folder%20with%20spaces/very%20empty%20folder",
location: "/Folder with spaces/very empty folder",
permissions: %i[readable writeable]),
ancestors: [
Results::StorageFileAncestor.new(name: "Root", location: "/"),
@@ -198,7 +198,7 @@ module Storages
last_modified_at: Time.zone.parse("2024-08-09T11:51:40Z"),
created_by_name: "Mara Jade",
last_modified_by_name: nil,
location: "/%c3%9cml%c3%a6%c3%bbts/Anr%c3%bcchiges%20deutsches%20Dokument.docx",
location: "/Ümlæûts/Anrüchiges deutsches Dokument.docx",
permissions: %i[readable writeable]),
Results::StorageFile.new(id: "563",
name: "data",
@@ -208,7 +208,7 @@ module Storages
last_modified_at: Time.zone.parse("2024-08-09T11:51:30Z"),
created_by_name: "Mara Jade",
last_modified_by_name: nil,
location: "/%c3%9cml%c3%a6%c3%bbts/data",
location: "/Ümlæûts/data",
permissions: %i[readable writeable])
],
parent: Results::StorageFile.new(id: "562",
@@ -219,7 +219,7 @@ module Storages
last_modified_at: Time.zone.parse("2024-08-09T11:51:48Z"),
created_by_name: "Mara Jade",
last_modified_by_name: nil,
location: "/%c3%9cml%c3%a6%c3%bbts",
location: "/Ümlæûts",
permissions: %i[readable writeable]),
ancestors: [
Results::StorageFileAncestor.new(name: "Root", location: "/")
@@ -51,6 +51,7 @@ RSpec.shared_examples_for "adapter create_folder_command: successful folder crea
expect(response).to be_a(Storages::Adapters::Results::StorageFile)
expect(response.name).to eq(folder_name)
expect(response.location).to eq(path)
expect(response.mime_type).to eq("application/x-op-directory")
ensure
delete_created_folder(response)
end