diff --git a/lib/api/CMakeLists.txt b/lib/api/CMakeLists.txt index 39f7f4d5d3..ee52b343d8 100644 --- a/lib/api/CMakeLists.txt +++ b/lib/api/CMakeLists.txt @@ -147,6 +147,7 @@ foreach(variant IN ITEMS client server) "${CMAKE_CURRENT_SOURCE_DIR}/src/rc_register_physical_path.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/rc_replica_close.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/rc_replica_open.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/rc_replica_truncate.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/rc_set_delay_server_migration_info.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/rc_set_grid_configuration_value.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/rc_switch_user.cpp" @@ -314,6 +315,7 @@ install( "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/register_physical_path.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/replica_close.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/replica_open.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/replica_truncate.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/rmColl.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/ruleExecDel.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/ruleExecMod.h" diff --git a/lib/api/include/irods/apiHeaderAll.h b/lib/api/include/irods/apiHeaderAll.h index ee49dff71a..77691d814b 100644 --- a/lib/api/include/irods/apiHeaderAll.h +++ b/lib/api/include/irods/apiHeaderAll.h @@ -148,4 +148,6 @@ #include "irods/check_auth_credentials.h" +#include "irods/replica_truncate.h" + #endif // API_HEADER_ALL_H__ diff --git a/lib/api/include/irods/apiNumberData.h b/lib/api/include/irods/apiNumberData.h index 409e53a8a7..0a9d771df7 100644 --- a/lib/api/include/irods/apiNumberData.h +++ b/lib/api/include/irods/apiNumberData.h @@ -146,6 +146,7 @@ API_NUMBER(GET_LIMITED_PASSWORD_AN, 726) API_NUMBER(CHECK_AUTH_CREDENTIALS_AN, 800) API_NUMBER(GET_LIBRARY_FEATURES_AN, 801) +API_NUMBER(REPLICA_TRUNCATE_AN, 802) /* 1100 - 1200 - SSL API calls */ API_NUMBER(SSL_START_AN, 1100) diff --git a/lib/api/include/irods/apiTable.hpp b/lib/api/include/irods/apiTable.hpp index 8b7816d6d1..2a25b70a4a 100644 --- a/lib/api/include/irods/apiTable.hpp +++ b/lib/api/include/irods/apiTable.hpp @@ -153,6 +153,7 @@ # include "irods/rs_check_auth_credentials.hpp" # include "irods/rs_get_library_features.hpp" # include "irods/rs_get_resource_info_for_operation.hpp" +# include "irods/rs_replica_truncate.hpp" # define NULLPTR_FOR_CLIENT_TABLE(x) x #elif !defined(CREATE_API_TABLE_FOR_SERVER) && defined(CREATE_API_TABLE_FOR_CLIENT) # define NULLPTR_FOR_CLIENT_TABLE(x) nullptr @@ -264,6 +265,7 @@ #define RS_REG_COLL NULLPTR_FOR_CLIENT_TABLE(rsRegColl) #define RS_REG_DATA_OBJ NULLPTR_FOR_CLIENT_TABLE(rsRegDataObj) #define RS_REG_REPLICA NULLPTR_FOR_CLIENT_TABLE(rsRegReplica) +#define RS_REPLICA_TRUNCATE NULLPTR_FOR_CLIENT_TABLE(rs_replica_truncate) #define RS_RM_COLL NULLPTR_FOR_CLIENT_TABLE(rsRmColl) #define RS_RULE_EXEC_DEL NULLPTR_FOR_CLIENT_TABLE(rsRuleExecDel) #define RS_RULE_EXEC_MOD NULLPTR_FOR_CLIENT_TABLE(rsRuleExecMod) @@ -1279,6 +1281,12 @@ static irods::apidef_t client_api_table_inp[] = { boost::any(std::function(RS_GET_RESOURCE_INFO_FOR_OPERATION)), "api_get_resource_info_for_operation", clearDataObjInp, irods::clearOutStruct_noop, (funcPtr)CALL_GET_RESOURCE_INFO_FOR_OPERATION + }, + { REPLICA_TRUNCATE_AN, RODS_API_VERSION, REMOTE_USER_AUTH, REMOTE_USER_AUTH, + "DataObjInp_PI", 0, "STR_PI", 0, + boost::any(std::function(RS_REPLICA_TRUNCATE)), + "api_replica_truncate", clearDataObjInp, irods::clearOutStruct_noop, + (funcPtr)CALL_REPLICA_TRUNCATE } // clang-format on }; // _api_table_inp diff --git a/lib/api/include/irods/replica_truncate.h b/lib/api/include/irods/replica_truncate.h new file mode 100644 index 0000000000..7223037d0f --- /dev/null +++ b/lib/api/include/irods/replica_truncate.h @@ -0,0 +1,89 @@ +#ifndef IRODS_REPLICA_TRUNCATE_H +#define IRODS_REPLICA_TRUNCATE_H + +/// \file + +#include "irods/dataObjInpOut.h" + +struct RcComm; + +#ifdef __cplusplus +extern "C" { +#endif + +/// Truncate a replica for the specified data object to the specified size. +/// +/// \parblock +/// This API selects a replica to truncate according to the rules of POSIX truncate(2). The caller may provide keywords +/// via condInput in order to influence the hierarchy resolution for selecting a replica to truncate. +/// \endparblock +/// +/// \param[in] _comm A pointer to a RcComm. +/// \param[in] _inp \parblock +/// DataObjInp structure which requires the following inputs: +/// objPath - The full logical path to the target data object. +/// dataSize - The desired size of the replica after truncating. +/// +/// The condInput supports the following keywords: +/// REPL_NUM_KW - The replica number of the replica to truncate. +/// RESC_NAME_KW - The name of the resource with the replica to truncate. Must be a root resource. +/// DEF_RESC_NAME_KW - The default resource to target in the absence of any other inputs or policy. +/// RESC_HIER_STR_KW - Full resource hierarchy to the replica to truncate. Use with caution. +/// ADMIN_KW - Flag indicating that the operation is to be performed with elevated privileges. No value required. +/// \endparblock +/// \param[out] _out \parblock +/// Character string representing a JSON structure with the following form: +/// \code{.js} +/// { +/// // Resource hierarchy of the selected replica for truncate. If an error occurs before hierarchy resolution is +/// // completed, a null value will be here instead of a string. +/// "resource_hierarchy": , +/// // Replica number of the selected replica for truncate. If an error occurs before hierarchy resolution is +/// // completed, a null value will be here instead of an integer. +/// "replica_number": , +/// // A string containing any relevant message the server may wish to send to the user (including error messages). +/// // This value will always be a string, even if it is empty. +/// "message": +/// } +/// \endcode +/// \endparblock +/// +/// \usage \parblock +/// \code{c} +/// RcComm* comm = // Our iRODS connection. +/// +/// // Don't forget to call clearKeyVal on truncate_doi.condInput before exiting to prevent leaks. +/// DataObjInp truncate_doi; +/// memset(&truncate_doi, 0, sizeof(DataObjInp)); +/// +/// // Set the path and the desired size. +/// strncpy(truncate_doi.objPath, "/tempZone/home/alice/foo", MAX_NAME_LEN); +/// truncate_doi.size = 0; +/// +/// // Target a specific replica, if desired. If no replica is targeted via some key-value pair, the policy for +/// // selecting a preferred resource configured on the server will be used for resolving the hierarchy for a +/// // "write" operation. +/// addKeyVal(&truncate_doi.condInput, REPL_NUM_KW, "3"); +/// +/// // Need a character string to hold the output. Don't forget to free this before exiting to prevent leaks. +/// char* output_string = NULL; +/// +/// const int ec = rc_replica_truncate(comm, &truncate_doi, &output_string); +/// if (ec < 0) { +/// // Error handling. Perhaps use the "message" field inside the output_string. +/// } +/// \endcode +/// \endparblock +/// +/// \return An integer representing an iRODS error code. +/// \retval 0 on success. +/// \retval <0 on failure. +/// +/// \since 4.3.2 +int rc_replica_truncate(struct RcComm* _comm, struct DataObjInp* _inp, char** _out); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // IRODS_REPLICA_TRUNCATE_H diff --git a/lib/api/src/rc_replica_truncate.cpp b/lib/api/src/rc_replica_truncate.cpp new file mode 100644 index 0000000000..19875025fd --- /dev/null +++ b/lib/api/src/rc_replica_truncate.cpp @@ -0,0 +1,14 @@ +#include "irods/replica_truncate.h" + +#include "irods/apiNumber.h" +#include "irods/procApiRequest.h" +#include "irods/rodsErrorTable.h" + +auto rc_replica_truncate(RcComm* _comm, DataObjInp* _inp, char** _out) -> int +{ + if (!_comm || !_inp || !_out) { + return SYS_INVALID_INPUT_PARAM; + } + + return procApiRequest(_comm, REPLICA_TRUNCATE_AN, _inp, nullptr, reinterpret_cast(_out), nullptr); +} // rc_replica_truncate diff --git a/lib/core/include/irods/library_features.h b/lib/core/include/irods/library_features.h index 5f1c603c45..bbf2f7f488 100644 --- a/lib/core/include/irods/library_features.h +++ b/lib/core/include/irods/library_features.h @@ -49,4 +49,9 @@ /// \since 4.3.1 #define IRODS_HAS_API_ENDPOINT_CHECK_AUTH_CREDENTIALS 202307L +/// Defined if the development library supports #rc_replica_truncate. +/// +/// \since 4.3.2 +#define IRODS_HAS_API_ENDPOINT_REPLICA_TRUNCATE 202403L + #endif // IRODS_LIBRARY_FEATURES_H diff --git a/server/api/CMakeLists.txt b/server/api/CMakeLists.txt index 5fe01a27ff..8afd065f77 100644 --- a/server/api/CMakeLists.txt +++ b/server/api/CMakeLists.txt @@ -13,6 +13,7 @@ add_library( "${CMAKE_CURRENT_SOURCE_DIR}/src/rs_register_physical_path.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/rs_replica_close.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/rs_replica_open.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/rs_replica_truncate.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/rs_set_delay_server_migration_info.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/rs_set_grid_configuration_value.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/rs_touch.cpp" @@ -211,8 +212,9 @@ install( "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/rs_get_library_features.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/rs_get_resource_info_for_operation.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/rs_register_physical_path.hpp" - "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/rs_replica_open.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/rs_replica_close.hpp" + "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/rs_replica_open.hpp" + "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/rs_replica_truncate.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/rs_set_delay_server_migration_info.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/rs_set_grid_configuration_value.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/include/irods/rs_touch.hpp" diff --git a/server/api/include/irods/rs_replica_truncate.hpp b/server/api/include/irods/rs_replica_truncate.hpp new file mode 100644 index 0000000000..0a425bcfb2 --- /dev/null +++ b/server/api/include/irods/rs_replica_truncate.hpp @@ -0,0 +1,90 @@ +#ifndef IRODS_RS_REPLICA_TRUNCATE_HPP +#define IRODS_RS_REPLICA_TRUNCATE_HPP + +/// \file + +#include "irods/dataObjInpOut.h" + +struct RsComm; + +#ifdef __cplusplus +extern "C" { +#endif + +/// Truncate a replica for the specified data object to the specified size. +/// +/// \parblock +/// This API selects a replica to truncate according to the rules of POSIX truncate(2). The caller may provide keywords +/// via condInput in order to influence the hierarchy resolution for selecting a replica to truncate. +/// \endparblock +/// +/// \param[in] _comm A pointer to a RsComm. +/// \param[in] _inp \parblock +/// DataObjInp structure which requires the following inputs: +/// objPath - The full logical path to the target data object. +/// dataSize - The desired size of the replica after truncating. +/// +/// The condInput supports the following keywords: +/// REPL_NUM_KW - The replica number of the replica to truncate. +/// RESC_NAME_KW - The name of the resource with the replica to truncate. Must be a root resource. +/// DEF_RESC_NAME_KW - The default resource to target in the absence of any other inputs or policy. +/// RESC_HIER_STR_KW - Full resource hierarchy to the replica to truncate. Use with caution. +/// ADMIN_KW - Flag indicating that the operation is to be performed with elevated privileges. No value required. +/// \endparblock +/// \param[out] _out \parblock +/// Character string representing a JSON structure with the following form: +/// \code{.js} +/// { +/// // Resource hierarchy of the selected replica for truncate. If an error occurs before hierarchy resolution is +/// // completed, a null value will be here instead of a string. +/// "resource_hierarchy": , +/// // Replica number of the selected replica for truncate. If an error occurs before hierarchy resolution is +/// // completed, a null value will be here instead of an integer. +/// "replica_number": , +/// // A string containing any relevant message the server may wish to send to the user (including error messages). +/// // This value will always be a string, even if it is empty. +/// "message": +/// } +/// \endcode +/// \endparblock +/// +/// \usage \parblock +/// \code{c} +/// // Because this is a server-side function, we will assume that there is access to an RsComm; +/// RsComm* comm; +/// +/// // Don't forget to call clearKeyVal on truncate_doi.condInput before exiting to prevent leaks. +/// DataObjInp truncate_doi; +/// memset(&truncate_doi, 0, sizeof(DataObjInp)); +/// +/// // Set the path and the desired size. +/// strncpy(truncate_doi.objPath, "/tempZone/home/alice/foo", MAX_NAME_LEN); +/// truncate_doi.size = 0; +/// +/// // Target a specific replica, if desired. If no replica is targeted via some key-value pair, the policy for +/// // selecting a preferred resource configured on the server will be used for resolving the hierarchy for a +/// // "write" operation. +/// addKeyVal(&truncate_doi.condInput, REPL_NUM_KW, "3"); +/// +/// // Need a character string to hold the output. Don't forget to free this before exiting to prevent leaks. +/// char* output_string = NULL; +/// +/// const int ec = rs_replica_truncate(comm, &truncate_doi, &output_string); +/// if (ec < 0) { +/// // Error handling. Perhaps use the "message" field inside the output_string. +/// } +/// \endcode +/// \endparblock +/// +/// \return An integer representing an iRODS error code, or 0. +/// \retval 0 on success. +/// \retval <0 on failure; an iRODS error code. +/// +/// \since 4.3.2 +int rs_replica_truncate(RsComm* _comm, DataObjInp* _inp, char** _out); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // IRODS_RS_REPLICA_TRUNCATE_HPP diff --git a/server/api/src/rs_get_library_features.cpp b/server/api/src/rs_get_library_features.cpp index ee65d069e3..e32a963f3b 100644 --- a/server/api/src/rs_get_library_features.cpp +++ b/server/api/src/rs_get_library_features.cpp @@ -27,6 +27,7 @@ auto rs_get_library_features(RsComm* _comm, char** _features) -> int IRODS_FEATURE(IRODS_HAS_LIBRARY_SYSTEM_ERROR) IRODS_FEATURE(IRODS_HAS_FEATURE_PROXY_USER_SUPPORT_FOR_CLIENT_CONNECTION_LIBRARIES) IRODS_FEATURE(IRODS_HAS_API_ENDPOINT_CHECK_AUTH_CREDENTIALS) + IRODS_FEATURE(IRODS_HAS_API_ENDPOINT_REPLICA_TRUNCATE) }.dump().c_str()); // clang-format on diff --git a/server/api/src/rs_replica_truncate.cpp b/server/api/src/rs_replica_truncate.cpp new file mode 100644 index 0000000000..e70e24c3db --- /dev/null +++ b/server/api/src/rs_replica_truncate.cpp @@ -0,0 +1,327 @@ +#include "irods/rs_replica_truncate.hpp" + +#include "irods/getRemoteZoneResc.h" +#include "irods/irods_at_scope_exit.hpp" +#include "irods/irods_error.hpp" +#include "irods/irods_exception.hpp" +#include "irods/irods_file_object.hpp" +#include "irods/irods_logger.hpp" +#include "irods/irods_resource_backport.hpp" +#include "irods/irods_resource_redirect.hpp" +#include "irods/irods_rs_comm_query.hpp" +#include "irods/logical_locking.hpp" +#include "irods/replica_truncate.h" +#include "irods/rodsConnect.h" +#include "irods/rsFileTruncate.hpp" +#include "irods/rsModDataObjMeta.hpp" + +#define IRODS_REPLICA_ENABLE_SERVER_SIDE_API +#include "irods/data_object_proxy.hpp" + +#include +#include + +#include +#include +#include // For std::tie. + +#include // For strdup. + +namespace +{ + namespace ill = irods::logical_locking; + using log_api = irods::experimental::log::api; + + auto make_json_output(const std::string& _message, const DataObjInfo* _replica = nullptr) -> nlohmann::json + { + constexpr auto json_null = nlohmann::json::value_t::null; + + nlohmann::json output; + + output["message"] = _message; + + if (_replica) { + output["resource_hierarchy"] = _replica->rescHier; + } + else { + output["resource_hierarchy"] = json_null; + } + + if (_replica) { + output["replica_number"] = _replica->replNum; + } + else { + output["replica_number"] = json_null; + } + + return output; + } // make_json_output + + auto get_data_object_info_and_resolve_hierarchy(RsComm* _comm, + DataObjInp* _inp, + DataObjInfo** _doi_out, + std::string& _hierarchy_out) -> int + { + if (!_comm || !_inp || !_doi_out) { + return SYS_INTERNAL_NULL_INPUT_ERR; + } + + *_doi_out = nullptr; + + if (const auto* provided_resc_hier = getValByKey(&_inp->condInput, RESC_HIER_STR_KW); provided_resc_hier) { + _hierarchy_out = provided_resc_hier; + } + + DataObjInfo* info_head = nullptr; + + if (_hierarchy_out.empty()) { + try { + irods::file_object_ptr file_obj; + std::tie(file_obj, _hierarchy_out) = + irods::resolve_resource_hierarchy(irods::WRITE_OPERATION, _comm, *_inp, &info_head); + } + catch (const irods::exception& e) { + if (info_head) { + freeAllDataObjInfo(info_head); + } + + // If the data object does not exist, then the exception will contain error code of CAT_NO_ROWS_FOUND. + if (e.code() == CAT_NO_ROWS_FOUND) { + return OBJ_PATH_DOES_NOT_EXIST; + } + + return static_cast(e.code()); + } + + addKeyVal(&_inp->condInput, RESC_HIER_STR_KW, _hierarchy_out.c_str()); + } + else { + // If the resource hierarchy has already been resolved, we still need the data object information. The + // file_object_factory is used here in order to ensure that the same information is used for both cases. + irods::file_object_ptr file_obj{new irods::file_object{}}; + if (const auto err = irods::file_object_factory(_comm, _inp, file_obj, &info_head); !err.ok()) { + if (info_head) { + freeAllDataObjInfo(info_head); + } + + return static_cast(err.code()); + } + } + + // If for whatever reason the function calls above did not produce an error but did not return a pointer to the + // data object information, throw an error here. + if (!info_head) { + return SYS_INTERNAL_NULL_INPUT_ERR; + } + + // An error should be returned if the resolved hierarchy had a locked or intermediate replica. However, not all + // code paths resolve a resource hierarchy and it is inexpensive to check, regardless. + if (const auto ec = ill::try_lock(*info_head, ill::lock_type::write, _hierarchy_out); ec < 0) { + return ec; + } + + *_doi_out = info_head; + + return 0; + } // get_data_object_info_and_resolve_hierarchy + + auto truncate_replica(RsComm* _comm, const DataObjInp* _inp, const DataObjInfo* _replica) -> int + { + if (!_comm || !_inp || !_replica) { + return SYS_INTERNAL_NULL_INPUT_ERR; + } + + std::string location; + if (const auto err = irods::get_loc_for_hier_string(_replica->rescHier, location); !err.ok()) { + return static_cast(err.code()); + } + + fileOpenInp_t file_open_inp{}; + std::strncpy(file_open_inp.fileName, _replica->filePath, MAX_NAME_LEN); + std::strncpy(file_open_inp.resc_hier_, _replica->rescHier, MAX_NAME_LEN); + std::strncpy(file_open_inp.addr.hostAddr, location.c_str(), NAME_LEN); + file_open_inp.dataSize = _inp->dataSize; + return rsFileTruncate(_comm, &file_open_inp); + } // truncate_replica + + auto update_system_metadata_for_data_object(RsComm* _comm, const DataObjInp* _inp, const DataObjInfo* _info) -> int + { + ModDataObjMetaInp mod_data_obj_meta_inp{}; + + auto kvp = KeyValPair{}; + const auto clear_kvp = irods::at_scope_exit{[&kvp] { clearKeyVal(&kvp); }}; + auto reg_param = irods::experimental::make_key_value_proxy(kvp); + + reg_param[DATA_SIZE_KW] = std::to_string(_inp->dataSize); + reg_param[CHKSUM_KW] = ""; + reg_param[OPEN_TYPE_KW] = std::to_string(OPEN_FOR_WRITE_TYPE); + reg_param[ALL_REPL_STATUS_KW] = ""; + reg_param[DATA_MODIFY_KW] = std::to_string(time(nullptr)); + if (getValByKey(&_inp->condInput, ADMIN_KW)) { + reg_param[ADMIN_KW] = ""; + } + + // We must const_cast here because rsModDataObjMeta does not accept a constant input. Therefore, the input must + // be non-const, so the members inside the input structure must also be non-const. + mod_data_obj_meta_inp.dataObjInfo = const_cast(_info); + mod_data_obj_meta_inp.regParam = reg_param.get(); + return rsModDataObjMeta(_comm, &mod_data_obj_meta_inp); + } // update_system_metadata_for_data_object +} // anonymous namespace + +auto rs_replica_truncate(RsComm* _comm, DataObjInp* _inp, char** _out) -> int +{ + if (!_comm || !_inp || !_out) { + static const auto msg = fmt::format("{}: Null input parameter.", __func__); + log_api::info(msg); + return USER__NULL_INPUT_ERR; + } + + *_out = nullptr; + + // Do not allow DEST_RESC_NAME_KW to be used. It is confusing when RESC_NAME_KW is also supported and the API is + // only dealing with one replica. + if (const auto* destination_resource = getValByKey(&_inp->condInput, DEST_RESC_NAME_KW); destination_resource) { + static const auto msg = fmt::format("{}: [{}] keyword not supported.", __func__, DEST_RESC_NAME_KW); + log_api::info(msg); + *_out = strdup(make_json_output(msg).dump().c_str()); + return SYS_INVALID_INPUT_PARAM; + } + + // Do not allow DEST_RESC_HIER_STR_KW to be used. It is confusing when RESC_HIER_STR_KW is also supported and the + // API is only dealing with one replica. + if (const auto* destination_hierarchy = getValByKey(&_inp->condInput, DEST_RESC_HIER_STR_KW); destination_hierarchy) + { + const auto msg = fmt::format("{}: [{}] keyword not supported.", __func__, DEST_RESC_HIER_STR_KW); + log_api::info(msg); + *_out = strdup(make_json_output(msg).dump().c_str()); + return SYS_INVALID_INPUT_PARAM; + } + + const auto* resource_name = getValByKey(&_inp->condInput, RESC_NAME_KW); + const auto* replica_number = getValByKey(&_inp->condInput, REPL_NUM_KW); + + // Do not allow RESC_NAME_KW and REPL_NUM_KW to be used at the same time. + if (resource_name && replica_number) { + static const auto msg = + fmt::format("{}: [{}] and [{}] keywords cannot be used together.", __func__, RESC_NAME_KW, REPL_NUM_KW); + log_api::info(msg); + *_out = strdup(make_json_output(msg).dump().c_str()); + return SYS_INVALID_INPUT_PARAM; + } + + // Check for admin keyword and only allow its usage if the connected client user is privileged. + if (getValByKey(&_inp->condInput, ADMIN_KW) && !irods::is_privileged_client(*_comm)) { + const auto msg = fmt::format("{}: [{}] keyword used by non-admin user [{}#{}].", + __func__, + ADMIN_KW, + _comm->clientUser.userName, + _comm->clientUser.rodsZone); + log_api::error(msg); + *_out = strdup(make_json_output(msg).dump().c_str()); + return CAT_INSUFFICIENT_PRIVILEGE_LEVEL; + } + + // Do not free this structure because it is a pointer to a global list of rodsServerHost structs which must + // persist for the life of the agent. + rodsServerHost* host_info = nullptr; + const auto remoteFlag = getAndConnRemoteZone(_comm, _inp, &host_info, REMOTE_OPEN); + if (remoteFlag < 0) { + const auto msg = + fmt::format("{}: getAndConnRemoteZone failed for [{}]: {}", __func__, _inp->objPath, remoteFlag); + log_api::error(msg); + *_out = strdup(make_json_output(msg).dump().c_str()); + return remoteFlag; + } + + // Let the remote zone handle it. + if (REMOTE_HOST == remoteFlag) { + return rc_replica_truncate(host_info->conn, _inp, _out); + } + + try { + // Get the data object information and resolve the resource hierarchy. If an error occurs, this function + // should throw. Cannot use structured bindings with these types. + DataObjInfo* doi = nullptr; + std::string resolved_hierarchy; + const auto free_data_object_info = irods::at_scope_exit{[&doi] { freeAllDataObjInfo(doi); }}; + if (const auto ec = get_data_object_info_and_resolve_hierarchy(_comm, _inp, &doi, resolved_hierarchy); ec < 0) { + const auto msg = fmt::format("{}: Error occurred resolving hierarchy or getting information for [{}]: {}", + __func__, + _inp->objPath, + ec); + log_api::error(msg); + *_out = strdup(make_json_output(msg).dump().c_str()); + return ec; + } + + // Find the replica in the list which resides on the resolved resource hierarchy. If it is not found, something + // has gone horribly wrong and we should bail. + const auto* replica = irods::experimental::data_object::find_replica(*doi, resolved_hierarchy); + if (!replica) { + const auto msg = fmt::format( + "{}: [{}] has no replica on resolved hierarchy [{}].", __func__, _inp->objPath, resolved_hierarchy); + log_api::error(msg); + *_out = strdup(make_json_output(msg).dump().c_str()); + return SYS_REPLICA_DOES_NOT_EXIST; + } + + // If the user specified a resource and it is not in the resolved hierarchy, this is considered an error. + if (resource_name && !irods::hierarchy_parser{replica->rescHier}.resc_in_hier(resource_name)) { + const auto msg = fmt::format( + "{}: Hierarchy descending from specified resource name [{}] does not have a replica of [{}] " + "or the replica is inaccessible at this time.", + __func__, + resource_name, + _inp->objPath); + log_api::error(msg); + *_out = strdup(make_json_output(msg, replica).dump().c_str()); + return SYS_REPLICA_INACCESSIBLE; + } + + // If the target replica is already of the specified size, there is nothing to do. + if (replica->dataSize == _inp->dataSize) { + const auto msg = fmt::format("{}: Replica of [{}] on [{}] already has size [{}].", + __func__, + _inp->objPath, + resolved_hierarchy, + _inp->dataSize); + log_api::debug(msg); + *_out = strdup(make_json_output(msg, replica).dump().c_str()); + return 0; + } + + if (const auto ec = truncate_replica(_comm, _inp, replica); ec < 0) { + const auto msg = fmt::format("{}: Error occurred while truncating replica of [{}] on [{}]: {}", + __func__, + replica->objPath, + replica->rescHier, + ec); + log_api::debug(msg); + *_out = strdup(make_json_output(msg, replica).dump().c_str()); + return ec; + } + + if (const auto ec = update_system_metadata_for_data_object(_comm, _inp, replica); ec < 0) { + const auto msg = + fmt::format("{}: Error occurred while updating system metadata for replica of [{}] on [{}]: {}", + __func__, + replica->objPath, + replica->rescHier, + ec); + log_api::debug(msg); + *_out = strdup(make_json_output(msg, replica).dump().c_str()); + return ec; + } + } + catch (const irods::exception& e) { + log_api::error("{}: Failed to truncate [{}]: {}", __func__, _inp->objPath, e.client_display_what()); + return static_cast(e.code()); + } + catch (const std::exception& e) { + log_api::error("{}: Exception occurred truncating [{}]: {}", __func__, _inp->objPath, e.what()); + return SYS_INTERNAL_ERR; + } + + return 0; +} // rs_replica_truncate diff --git a/server/core/include/irods/irods_api_calling_functions.hpp b/server/core/include/irods/irods_api_calling_functions.hpp index f320774db2..a87c5b23d8 100644 --- a/server/core/include/irods/irods_api_calling_functions.hpp +++ b/server/core/include/irods/irods_api_calling_functions.hpp @@ -1045,4 +1045,11 @@ int call_get_resource_info_for_operation(irods::api_entry*, rsComm_t*, dataObjIn # define CALL_GET_RESOURCE_INFO_FOR_OPERATION nullptr // NOLINT(cppcoreguidelines-macro-usage) #endif +#ifdef CREATE_API_TABLE_FOR_SERVER +int call_replica_truncate(irods::api_entry*, rsComm_t*, dataObjInp_t*, char**); +# define CALL_REPLICA_TRUNCATE call_replica_truncate +#else +# define CALL_REPLICA_TRUNCATE nullptr // NOLINT(cppcoreguidelines-macro-usage) +#endif + #endif // IRODS_API_CALLING_FUNCTIONS_HPP diff --git a/server/core/src/client_api_allowlist.cpp b/server/core/src/client_api_allowlist.cpp index 188c112d0d..f8b9eaa905 100644 --- a/server/core/src/client_api_allowlist.cpp +++ b/server/core/src/client_api_allowlist.cpp @@ -183,7 +183,8 @@ namespace irods::client_api_allowlist GET_LIBRARY_FEATURES_AN, - GET_RESOURCE_INFO_FOR_OPERATION_AN + GET_RESOURCE_INFO_FOR_OPERATION_AN, + REPLICA_TRUNCATE_AN }; // clang-format on } // init diff --git a/server/core/src/irods_api_calling_functions.cpp b/server/core/src/irods_api_calling_functions.cpp index 78f878b8e9..95a4eb3a5b 100644 --- a/server/core/src/irods_api_calling_functions.cpp +++ b/server/core/src/irods_api_calling_functions.cpp @@ -1136,3 +1136,8 @@ int call_get_resource_info_for_operation(irods::api_entry* _api, rsComm_t* _comm { return _api->call_handler(_comm, _inp, _out); } // call_get_resource_info_for_operation + +auto call_replica_truncate(irods::api_entry* _api, rsComm_t* _comm, dataObjInp_t* _inp, char** _out) -> int +{ + return _api->call_handler(_comm, _inp, _out); +} // call_replica_truncate