diff --git a/src/endpoints/data_objects/impl.cpp b/src/endpoints/data_objects/impl.cpp index 77f201a4..96a60540 100644 --- a/src/endpoints/data_objects/impl.cpp +++ b/src/endpoints/data_objects/impl.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,7 @@ #include +#include #include #include #include @@ -187,6 +189,8 @@ namespace IRODS_HTTP_API_ENDPOINT_OPERATION_SIGNATURE(op_modify_metadata); + IRODS_HTTP_API_ENDPOINT_OPERATION_SIGNATURE(op_modify_replica); + // // Operation to Handler mappings // @@ -219,6 +223,8 @@ namespace {"calculate_checksum", op_calculate_checksum}, {"modify_metadata", op_modify_metadata}, + + {"modify_replica", op_modify_replica}, }; } // anonymous namespace @@ -1845,4 +1851,132 @@ namespace using namespace irods::http::shared_api_operations; return op_atomic_apply_metadata_operations(_sess_ptr, _req, _args, entity_type::data_object); } // op_modify_metadata + + IRODS_HTTP_API_ENDPOINT_OPERATION_SIGNATURE(op_modify_replica) + { + auto result = irods::http::resolve_client_identity(_req); + if (result.response) { + return _sess_ptr->send(std::move(*result.response)); + } + + const auto* client_info = result.client_info; + + irods::http::globals::background_task([fn = __func__, client_info, _sess_ptr, _req = std::move(_req), _args = std::move(_args)] { + log::info("{}: client_info->username = [{}]", fn, client_info->username); + + http::response res{http::status::ok, _req.version()}; + res.set(http::field::server, irods::http::version::server_name); + res.set(http::field::content_type, "application/json"); + res.keep_alive(_req.keep_alive()); + + try { + DataObjInfo info{}; + irods::at_scope_exit free_memory{[&info] { clearKeyVal(&info.condInput); }}; + + if (const auto iter = _args.find("lpath"); iter != std::end(_args)) { + std::strncpy(info.objPath, iter->second.c_str(), sizeof(DataObjInfo::objPath)); + } + else { + log::error("{}: Missing [lpath] parameter.", fn); + return _sess_ptr->send(irods::http::fail(res, http::status::bad_request)); + } + + if (auto iter = _args.find("resource-hierarchy"); iter != std::end(_args)) { + std::strncpy(info.rescHier, iter->second.c_str(), sizeof(DataObjInfo::rescHier)); + } + else if (iter = _args.find("replica-number"); iter != std::end(_args)) { + info.replNum = std::stoi(iter->second.c_str()); + } + else { + log::error("{}: Missing [resource-hierarchy] or [replica-number] parameter.", fn); + return _sess_ptr->send(irods::http::fail(res, http::status::bad_request)); + } + + KeyValPair kvp{}; + irods::at_scope_exit clear_kvp{[&kvp] { clearKeyVal(&kvp); }}; + + if (const auto name_iter = _args.find("property-name"); name_iter != std::end(_args)) { + static constexpr auto properties = std::to_array>({ + {"COLL_ID", COLL_ID_KW}, + {"DATA_CREATE_TIME", DATA_CREATE_KW}, + {"DATA_CHECKSUM", CHKSUM_KW}, + {"DATA_EXPIRY", DATA_EXPIRY_KW}, + {"DATA_ID", DATA_ID_KW}, + {"DATA_REPL_STATUS", REPL_STATUS_KW}, + {"DATA_MAP_ID", DATA_MAP_ID_KW}, + {"DATA_MODE", DATA_MODE_KW}, + {"DATA_NAME", DATA_NAME_KW}, + {"DATA_OWNER_NAME", DATA_OWNER_KW}, + {"DATA_OWNER_ZONE", DATA_OWNER_ZONE_KW}, + {"DATA_PATH", FILE_PATH_KW}, + {"DATA_REPL_NUM", REPL_NUM_KW}, + {"DATA_SIZE", DATA_SIZE_KW}, + {"DATA_STATUS", STATUS_STRING_KW}, + {"DATA_TYPE_NAME", DATA_TYPE_KW}, + {"DATA_VERSION", VERSION_KW}, + {"DATA_MODIFY_TIME", DATA_MODIFY_KW}, + {"DATA_COMMENTS", DATA_COMMENTS_KW}, + //{"DATA_RESC_GROUP_NAME", DATA_RESC_GROUP_NAME_KW}, // missing from genquery since 4.2 + {"DATA_RESC_HIER", RESC_HIER_STR_KW}, + {"DATA_RESC_ID", RESC_ID_KW}, + {"DATA_RESC_NAME", RESC_NAME_KW} + }); + + const auto end = std::end(properties); + const auto iter = std::find_if(std::begin(properties), end, [&name_iter](auto&& _pn) { + return name_iter->second == _pn.first; + }); + + if (iter == end) { + log::error("{}: Invalid value [{}] for [property-name] parameter.", fn, name_iter->second); + return _sess_ptr->send(irods::http::fail(res, http::status::bad_request)); + } + + if (const auto value_iter = _args.find("property-value"); value_iter != std::end(_args)) { + addKeyVal(&kvp, iter->second, value_iter->second.c_str()); + addKeyVal(&kvp, ADMIN_KW, ""); + } + else { + log::error("{}: Missing [property-value] parameter.", fn); + return _sess_ptr->send(irods::http::fail(res, http::status::bad_request)); + } + } + else { + log::error("{}: Missing [property-name] parameter.", fn); + return _sess_ptr->send(irods::http::fail(res, http::status::bad_request)); + } + + ModDataObjMetaInp input{}; + input.dataObjInfo = &info; + input.regParam = &kvp; + + auto conn = irods::get_connection(client_info->username); + const auto ec = rcModDataObjMeta(static_cast(conn), &input); + + json response{ + {"irods_response", { + {"error_code", ec} + }} + }; + + res.body() = response.dump(); + } + catch (const irods::exception& e) { + res.result(http::status::bad_request); + res.body() = json{ + {"irods_response", { + {"error_code", e.code()}, + {"error_message", e.client_display_what()} + }} + }.dump(); + } + catch (const std::exception& e) { + res.result(http::status::internal_server_error); + } + + res.prepare_payload(); + + _sess_ptr->send(std::move(res)); + }); + } // op_modify_replica } // anonymous namespace diff --git a/test/test_irods_http_api.py b/test/test_irods_http_api.py index cfbdb832..ac6625b5 100644 --- a/test/test_irods_http_api.py +++ b/test/test_irods_http_api.py @@ -848,6 +848,65 @@ def test_modifying_permissions_atomically(self): self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['irods_response']['error_code'], 0) + def test_modifying_replica_properties(self): + headers = {'Authorization': 'Bearer ' + self.rodsadmin_bearer_token} + + # Create a data object. + data_object = os.path.join('/', self.zone_name, 'home', self.rodsadmin_username, 'modrepl.txt') + r = requests.post(self.url_endpoint, headers=headers, data={ + 'op': 'touch', + 'lpath': data_object + }) + #print(r.content) # Debug + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json()['irods_response']['error_code'], 0) + + # Show the replica is currently marked as good. + r = requests.get(f'{self.url_base}/query', headers=headers, params={ + 'op': 'execute_genquery', + 'query': f"select DATA_REPL_STATUS where COLL_NAME = '{os.path.dirname(data_object)}' and DATA_NAME = '{os.path.basename(data_object)}'" + }) + #print(r.content) # Debug + self.assertEqual(r.status_code, 200) + + result = r.json() + self.assertEqual(result['irods_response']['error_code'], 0) + self.assertEqual(result['rows'][0][0], '1') + + # Change the replica's status to stale using the modify_replica operation. + r = requests.post(self.url_endpoint, headers=headers, data={ + 'op': 'modify_replica', + 'lpath': data_object, + 'replica-number': 0, + 'property-name': 'DATA_REPL_STATUS', + 'property-value': 0 + }) + #print(r.content) # Debug + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json()['irods_response']['error_code'], 0) + + # Show the replica is now marked stale. + r = requests.get(f'{self.url_base}/query', headers=headers, params={ + 'op': 'execute_genquery', + 'query': f"select DATA_REPL_STATUS where COLL_NAME = '{os.path.dirname(data_object)}' and DATA_NAME = '{os.path.basename(data_object)}'" + }) + #print(r.content) # Debug + self.assertEqual(r.status_code, 200) + + result = r.json() + self.assertEqual(result['irods_response']['error_code'], 0) + self.assertEqual(result['rows'][0][0], '0') + + # Remove the data object. + r = requests.post(self.url_endpoint, headers=headers, data={ + 'op': 'remove', + 'lpath': data_object, + 'no-trash': 1 + }) + #print(r.content) # Debug + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json()['irods_response']['error_code'], 0) + @unittest.skip('Test needs to be implemented.') def test_return_error_on_missing_parameters(self): pass