#include "remote/WebDav.hpp" #include "JSON.hpp" #include "curl/curl.hpp" #include "error.hpp" #include "logging/logger.hpp" #include "remote/remote.hpp" #include "strings/strings.hpp" #include "stringutil.hpp" #include "ui/PopMessageManager.hpp" #include // Declarations here. Definitions at bottom. /// @brief Gets a XML element by name. This is namespace agnostic. /// @param parent Parent XML element. /// @param name Name of the element to get. No namespace needed. static tinyxml2::XMLElement *get_element_by_name(tinyxml2::XMLElement *parent, std::string_view name); /// @brief Returns the starting point of the tag name without the namespace. /// @param tag Tag to get the name of. static std::string_view get_tag_begin(std::string_view tag); /// @brief Ensures the parent is a valid path. I guess some servers don't have trailing slashes for directories. /// @param parent Parent string. static std::string ensure_valid_dir_path(std::string_view parent); /// @brief Slices and unescapes the directory or filename from the href. /// @param handle Curl handle to use to unescape. /// @param href HREF to slice. /// @note This seemed like a better alternative than relying on servers having displayname. static std::string slice_name_from_href(curl::Handle &handle, std::string_view href); remote::WebDav::WebDav() : Storage("[WD]") { static constexpr const char *STRING_CONFIG_READ_ERROR = "Error initializing WebDav: %s"; json::Object config = json::new_object(json_object_from_file, remote::PATH_WEBDAV_CONFIG.data()); if (!config) { logger::log(STRING_CONFIG_READ_ERROR, "Error reading configuration file!"); return; } // Let's just get this all out of the way at once. json_object *origin = json::get_object(config, "origin"); json_object *basepath = json::get_object(config, "basepath"); json_object *username = json::get_object(config, "username"); json_object *password = json::get_object(config, "password"); if (!origin) { logger::log(STRING_CONFIG_READ_ERROR, "Config is missing origin!"); return; } m_origin = json_object_get_string(origin); if (basepath) { // The root is both in the beginning. I want this to work as closely as the original just not as poorly written // or thought out as the original JKSV dav code. m_root = stringutil::get_formatted_string("/%s/", json_object_get_string(basepath)); m_parent = m_root; } if (username) { m_username = json_object_get_string(username); } if (password) { m_password = json_object_get_string(password); } // This is the starting point. This will read the entire basepath listing in one go. remote::URL url{m_origin}; url.append_path(m_root).append_slash(); // This'll recursively get the full listing of the basepath for the WebDav server. std::string xml{}; const bool propFind = WebDav::prop_find(url, xml); const bool xmlProcessed = propFind && WebDav::process_listing(xml); if (!propFind || !xmlProcessed) { return; } m_isInitialized = true; } bool remote::WebDav::create_directory(std::string_view name) { static constexpr const char *STRING_CREATE_DIR_ERROR = "Error creating WebDav directory: %s"; std::string escapedName{}; const bool nameEscaped = curl::escape_string(m_curl, name, escapedName); if (!nameEscaped) { logger::log(STRING_CREATE_DIR_ERROR, "Error escaping directory name!"); return false; } remote::URL url{m_origin}; url.append_path(m_parent).append_path(escapedName).append_slash(); curl::reset_handle(m_curl); WebDav::append_credentials(); curl::set_option(m_curl, CURLOPT_URL, url.get()); curl::set_option(m_curl, CURLOPT_CUSTOMREQUEST, "MKCOL"); if (!curl::perform(m_curl)) { return false; } const long code = curl::get_response_code(m_curl); if (code != 201) { logger::log(STRING_CREATE_DIR_ERROR, name.data()); return false; } // This is the ID string so we can make WebDav work within the same framework as Google Drive. std::string id = m_parent + "/" + escapedName + "/"; m_list.emplace_back(name, id, m_parent, 0, true); return true; } bool remote::WebDav::upload_file(const fslib::Path &source, std::string_view remoteName, sys::ProgressTask *task) { static constexpr const char *STRING_ERROR_UPLOADING = "Error uploading to WebDav: %s"; fslib::File sourceFile{source, FsOpenMode_Read}; if (error::fslib(sourceFile.is_open())) { return false; } std::string escapedName{}; const bool nameEscaped = curl::escape_string(m_curl, remoteName, escapedName); if (!nameEscaped) { logger::log(STRING_ERROR_UPLOADING, "Failed to escape filename!"); return false; } if (task) { task->reset(static_cast(sourceFile.get_size())); } remote::URL url{m_origin}; url.append_path(m_parent).append_path(escapedName); curl::UploadStruct uploadData{.source = &sourceFile, .task = task}; curl::reset_handle(m_curl); WebDav::append_credentials(); curl::set_option(m_curl, CURLOPT_URL, url.get()); curl::set_option(m_curl, CURLOPT_UPLOAD, 1L); curl::set_option(m_curl, CURLOPT_UPLOAD_BUFFERSIZE, Storage::SIZE_UPLOAD_BUFFER); curl::set_option(m_curl, CURLOPT_READFUNCTION, curl::read_data_from_file); curl::set_option(m_curl, CURLOPT_READDATA, &uploadData); if (!curl::perform(m_curl)) { return false; } const std::string id = m_parent + "/" + escapedName; m_list.emplace_back(remoteName, id, m_parent, sourceFile.get_size(), false); return true; } bool remote::WebDav::patch_file(remote::Item *item, const fslib::Path &source, sys::ProgressTask *task) { static constexpr const char *STRING_ERROR_PATCHING = "Error patching file: %s"; fslib::File sourceFile{source, FsOpenMode_Read}; if (!sourceFile) { logger::log(STRING_ERROR_PATCHING, fslib::error::get_string()); return false; } if (task) { task->reset(static_cast(sourceFile.get_size())); } remote::URL url{m_origin}; url.append_path(item->get_id()); curl::UploadStruct uploadData{.source = &sourceFile, .task = task}; curl::reset_handle(m_curl); WebDav::append_credentials(); curl::set_option(m_curl, CURLOPT_URL, url.get()); curl::set_option(m_curl, CURLOPT_UPLOAD, 1L); curl::set_option(m_curl, CURLOPT_UPLOAD_BUFFERSIZE, Storage::SIZE_UPLOAD_BUFFER); curl::set_option(m_curl, CURLOPT_READFUNCTION, curl::read_data_from_file); curl::set_option(m_curl, CURLOPT_READDATA, &uploadData); if (!curl::perform(m_curl)) { return false; } // Just update the size this time. item->set_size(sourceFile.get_size()); return true; } bool remote::WebDav::download_file(const remote::Item *item, const fslib::Path &destination, sys::ProgressTask *task) { static constexpr const char *STRING_ERROR_DOWNLOADING = "Error downloading file: %s"; const int64_t itemSize = item->get_size(); fslib::File destFile{destination, FsOpenMode_Create | FsOpenMode_Write, itemSize}; if (!destFile) { logger::log(STRING_ERROR_DOWNLOADING, fslib::error::get_string()); return false; } if (task) { task->reset(static_cast(itemSize)); } remote::URL url{m_origin}; url.append_path(item->get_id()); curl::DownloadStruct download{.dest = &destFile, .task = task, .fileSize = itemSize}; curl::reset_handle(m_curl); WebDav::append_credentials(); curl::set_option(m_curl, CURLOPT_HTTPGET, 1L); curl::set_option(m_curl, CURLOPT_URL, url.get()); curl::set_option(m_curl, CURLOPT_WRITEFUNCTION, curl::download_file_threaded); curl::set_option(m_curl, CURLOPT_WRITEDATA, &download); std::thread writeThread{curl::download_write_thread_function, std::ref(download)}; if (!curl::perform(m_curl)) { return false; } // Copied from gd.cpp implementation. // TODO: Not sure how a thread helps if this parent waits here. // TODO: Read and understand what's actually happening before making comments on other's choices. writeThread.join(); return true; } bool remote::WebDav::delete_item(const remote::Item *item) { static constexpr const char *STRING_ERROR_DELETING = "Error deleting item: %s"; remote::URL url{m_origin}; url.append_path(item->get_id()); if (item->is_directory()) { url.append_slash(); } curl::reset_handle(m_curl); WebDav::append_credentials(); curl::set_option(m_curl, CURLOPT_CUSTOMREQUEST, "DELETE"); curl::set_option(m_curl, CURLOPT_URL, url.get()); if (!curl::perform(m_curl)) { return false; } const long code = curl::get_response_code(m_curl); if (code != 204) // This can be any of these, I think? { logger::log(STRING_ERROR_DELETING, "Deletion failed!"); return false; } auto findFile = Storage::find_file_by_name(item->get_name()); if (findFile == m_list.end()) { return false; } m_list.erase(findFile); return true; } bool remote::WebDav::rename_item(remote::Item *item, std::string_view newName) { std::string escapedName{}; const bool escaped = curl::escape_string(m_curl, newName, escapedName); if (!escaped) { return false; } remote::URL url{m_origin}; url.append_path(item->get_id()); remote::URL destLocation{m_origin}; destLocation.append_path(m_parent).append_path(escapedName); if (item->is_directory()) { url.append_slash(); destLocation.append_slash(); } const std::string destHeader = stringutil::get_formatted_string("Destination: %s", destLocation.get()); curl::HeaderList header = curl::new_header_list(); curl::append_header(header, destHeader); curl::reset_handle(m_curl); WebDav::append_credentials(); curl::set_option(m_curl, CURLOPT_CUSTOMREQUEST, "MOVE"); curl::set_option(m_curl, CURLOPT_HTTPHEADER, header.get()); curl::set_option(m_curl, CURLOPT_URL, url.get()); if (!curl::perform(m_curl)) { return false; } const long code = curl::get_response_code(m_curl); if (code != 201 && code != 204) { return false; } std::string newId{}; if (item->is_directory()) { newId = m_parent + escapedName + "/"; // Need to make sure the parents match too. remote::Storage::DirectoryListing dirListing{}; Storage::get_directory_listing_with_parent(item, dirListing); for (remote::Item *item : dirListing) { item->set_parent_id(newId); } } else { newId = m_parent + escapedName; } item->set_name(newName); item->set_id(newId); return false; } void remote::WebDav::append_credentials() { if (!m_username.empty()) { curl::set_option(m_curl, CURLOPT_USERNAME, m_username.c_str()); } if (!m_password.empty()) { curl::set_option(m_curl, CURLOPT_PASSWORD, m_password.c_str()); } } bool remote::WebDav::prop_find(const remote::URL &url, std::string &xml) { // Some servers block Depth: Infinity. curl::HeaderList header = curl::new_header_list(); curl::append_header(header, "Depth: 1"); curl::reset_handle(m_curl); WebDav::append_credentials(); curl::set_option(m_curl, CURLOPT_CUSTOMREQUEST, "PROPFIND"); curl::set_option(m_curl, CURLOPT_HTTPHEADER, header.get()); curl::set_option(m_curl, CURLOPT_URL, url.get()); curl::set_option(m_curl, CURLOPT_WRITEFUNCTION, curl::write_response_string); curl::set_option(m_curl, CURLOPT_WRITEDATA, &xml); return curl::perform(m_curl); } bool remote::WebDav::process_listing(std::string_view xml) { static constexpr const char *STRING_ERROR_PROCESSING_XML = "Error processing XML: %s"; // These are so string_views aren't constructed every loop. static constexpr std::string_view tagHref{"href"}; static constexpr std::string_view tagPropStat{"propstat"}; static constexpr std::string_view tagProp{"prop"}; static constexpr std::string_view tagResourceType{"resourcetype"}; static constexpr std::string_view tagCollection{"collection"}; static constexpr std::string_view tagGetContentLength{"getcontentlength"}; tinyxml2::XMLDocument listing{}; if (listing.Parse(xml.data(), xml.length())) { logger::log(STRING_ERROR_PROCESSING_XML, "Couldn't parse XML!"); return false; } tinyxml2::XMLElement *root = listing.RootElement(); tinyxml2::XMLElement *parent = root->FirstChildElement(); tinyxml2::XMLElement *parentLocation = get_element_by_name(parent, tagHref); if (!parentLocation) { logger::log(STRING_ERROR_PROCESSING_XML, "Error finding list parent location!"); return false; } const std::string parentID = ensure_valid_dir_path(parentLocation->GetText()); tinyxml2::XMLElement *current{}; for (current = parent->NextSiblingElement(); current; current = current->NextSiblingElement()) { // Parsing XML is actually annoying. Even with tinyxml2. tinyxml2::XMLElement *href = get_element_by_name(current, tagHref); tinyxml2::XMLElement *propstat = get_element_by_name(current, tagPropStat); tinyxml2::XMLElement *prop = get_element_by_name(propstat, tagProp); tinyxml2::XMLElement *resourceType = get_element_by_name(prop, tagResourceType); if (!href || !propstat || !prop || !resourceType) { continue; } // Avoid redundant calls the GetText later. const char *hrefText = href->GetText(); const std::string name = slice_name_from_href(m_curl, hrefText); tinyxml2::XMLElement *collection = get_element_by_name(resourceType, tagCollection); if (collection) { const std::string idString = ensure_valid_dir_path(hrefText); m_list.emplace_back(name, idString, parentID, 0, true); remote::URL nextUrl{m_origin}; nextUrl.append_path(hrefText); std::string xml{}; const bool propFind = WebDav::prop_find(nextUrl, xml); const bool processXml = propFind && WebDav::process_listing(xml); if (!propFind || !processXml) { logger::log(STRING_ERROR_PROCESSING_XML, hrefText); } } else { tinyxml2::XMLElement *getContentLength = get_element_by_name(prop, tagGetContentLength); if (!getContentLength) { continue; } const char *lengthString = getContentLength->GetText(); const int64_t contentLength = std::strtoll(lengthString, nullptr, 10); m_list.emplace_back(name, hrefText, parentID, contentLength, false); } } return true; } static tinyxml2::XMLElement *get_element_by_name(tinyxml2::XMLElement *parent, std::string_view name) { tinyxml2::XMLElement *current = parent->FirstChildElement(); if (!current) { return nullptr; } do { if (get_tag_begin(current->Name()) == name) { return current; } } while ((current = current->NextSiblingElement())); return nullptr; } static std::string_view get_tag_begin(std::string_view tag) { size_t colon = tag.find_first_of(':'); if (colon == tag.npos) { return tag; } return tag.substr(colon + 1); } static std::string ensure_valid_dir_path(std::string_view parent) { std::string returnParent{parent}; if (returnParent.back() != '/') { returnParent.append("/"); } return returnParent; } static std::string slice_name_from_href(curl::Handle &handle, std::string_view href) { std::string name{}; if (href.back() == '/') { // To do: This might not be the safest. size_t end = href.find_last_of('/') - 1; size_t begin = href.find_last_of('/', end); if (begin == href.npos) { name = href; } else { name = href.substr(begin + 1, (end - begin)); } } else { size_t begin = href.find_last_of('/'); if (begin == href.npos) { name = href; } else { name = href.substr(begin + 1); } } curl::unescape_string(handle, name, name); return name; }