XRootD
Loading...
Searching...
No Matches
XrdClHttpTape.cc
Go to the documentation of this file.
1/******************************************************************************/
2/* */
3/* X r d C l H t t p T a p e . c c */
4/* */
5/* (c) 2026 by the XRootD Collaboration */
6/* */
7/* This file is part of the XRootD software suite. */
8/* */
9/* XRootD is free software: you can redistribute it and/or modify it under */
10/* the terms of the GNU Lesser General Public License as published by the */
11/* Free Software Foundation, either version 3 of the License, or (at your */
12/* option) any later version. */
13/* */
14/* XRootD is distributed in the hope that it will be useful, but WITHOUT */
15/* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or */
16/* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public */
17/* License for more details. */
18/* */
19/* You should have received a copy of the GNU Lesser General Public License */
20/* along with XRootD in a file called COPYING.LESSER (LGPL license) and file */
21/* COPYING (GPL license). If not, see <http://www.gnu.org/licenses/>. */
22/* */
23/******************************************************************************/
24
25#include "XrdClHttpTape.hh"
26
27#include "XrdClHttpUtil.hh"
28
30#include "XrdCl/XrdClUtils.hh"
31#include "XrdCl/XrdClURL.hh"
32#include "XrdOuc/XrdOucJson.hh"
33#include "XrdOuc/XrdOucUtils.hh"
34
35#include <algorithm>
36#include <array>
37#include <cerrno>
38#include <cctype>
39#include <cstdint>
40#include <ctime>
41#include <limits>
42#include <memory>
43#include <new>
44#include <sstream>
45#include <stdexcept>
46#include <string>
47#include <string_view>
48#include <strings.h>
49#include <utility>
50
51#include <curl/curl.h>
52
53namespace
54{
55using Json = nlohmann::json;
56
57constexpr std::array<std::string_view, 6> kTapeLocalities = {{
58 "DISK", "TAPE", "DISK_AND_TAPE", "LOST", "NONE", "UNAVAILABLE"
59}};
60
61struct TapeEndpoint
62{
63 std::string uri;
64 std::string version;
65 std::string sitename;
66};
67
68struct StageFileSpec
69{
70 std::string url;
71 std::string path;
72 std::string diskLifetime;
73 Json targetedMetadata;
74};
75
76std::string NormalizeTapePath(const std::string &path)
77{
78 std::string result = path.empty() ? "/" : path;
79 if(result.front() != '/') result.insert(result.begin(), '/');
80 return XrdOucUtils::NormalizePath(result);
81}
82
83std::string PercentEncodeUrlPathSegment(const std::string &value, CURL *curl)
84{
85 if(!curl) throw std::bad_alloc();
86
87 char *escaped =
88 curl_easy_escape(curl, value.c_str(), static_cast<int>(value.size()));
89 if(!escaped) throw std::bad_alloc();
90 std::string result(escaped);
91 curl_free(escaped);
92 return result;
93}
94
95bool IsHttpEndpointUri(const std::string &uri)
96{
97 XrdCl::URL url(uri);
98 if(!url.IsValid() || url.GetHostName().empty()) return false;
99
100 const std::string &protocol = url.GetProtocol();
101 return strcasecmp(protocol.c_str(), "http") == 0
102 || strcasecmp(protocol.c_str(), "https") == 0;
103}
104
105bool UrlEndpointAndPath(const std::string &input, std::string &endpoint,
106 std::string &path, std::string &error)
107{
108 XrdCl::URL url(input);
109 if(!url.IsValid() || url.GetHostName().empty())
110 {
111 error = "invalid URL '" + input + "'";
112 return false;
113 }
114
115 std::string protocol = url.GetProtocol();
116 std::transform(protocol.begin(), protocol.end(), protocol.begin(),
117 [](unsigned char c) { return std::tolower(c); });
118 if(protocol == "davs") protocol = "https";
119 else if(protocol == "dav") protocol = "http";
120
121 if(protocol != "http" && protocol != "https")
122 {
123 error = "unsupported URL protocol '" + url.GetProtocol()
124 + "' for Tape REST API";
125 return false;
126 }
127
128 std::ostringstream out;
129 out << protocol << "://" << url.GetHostName();
130 if(url.GetPort() > 0) out << ":" << url.GetPort();
131 endpoint = out.str();
132 path = NormalizeTapePath(url.GetPath());
133 return true;
134}
135
136std::string FormatProblemResponse(long statusCode, const std::string &body)
137{
138 std::string detail;
139
140 if(!body.empty())
141 {
142 const Json json = Json::parse(body, nullptr, false);
143 if(!json.is_discarded() && json.is_object())
144 {
145 const auto title = json.find("title");
146 const auto problemDetail = json.find("detail");
147
148 if(title != json.end() && title->is_string())
149 {
150 detail = title->get<std::string>();
151 }
152 if(problemDetail != json.end() && problemDetail->is_string())
153 {
154 if(!detail.empty()) detail += " - ";
155 detail += problemDetail->get<std::string>();
156 }
157 }
158 if(detail.empty()) detail = body;
159 }
160
161 std::ostringstream out;
162 out << "HTTP " << statusCode;
163 if(!detail.empty()) out << ": " << detail;
164 return out.str();
165}
166
167XrdCl::XRootDStatus ErrorStatus(uint16_t code, const std::string &message)
168{
169 uint32_t errorNumber = EIO;
170 switch(code)
171 {
172 case XrdCl::errInvalidArgs: errorNumber = EINVAL; break;
173 case XrdCl::errNotSupported: errorNumber = ENOTSUP; break;
174 case XrdCl::errNotImplemented: errorNumber = ENOSYS; break;
175 case XrdCl::errInvalidResponse: errorNumber = EBADMSG; break;
176 default: break;
177 }
178 return XrdCl::XRootDStatus(
179 XrdCl::stError, code, errorNumber, message);
180}
181
182XrdCl::XRootDStatus ParseJsonResponse(const std::string &body,
183 const std::string &responseName,
184 Json &json)
185{
186 json = Json::parse(body, nullptr, false);
187 if(!json.is_discarded()) return XrdCl::XRootDStatus();
188
189 return ErrorStatus(XrdCl::errInvalidResponse,
190 "malformed " + responseName + " response");
191}
192
193const std::string kStructuredStagePrefix = "xrdclhttp.tape.stage:";
194
195bool IsValidRequestId(const std::string &requestId)
196{
197 return !requestId.empty()
198 && requestId.find_first_of("\r\n") == std::string::npos;
199}
200
201XrdCl::XRootDStatus ValidateTapePrepareFlags(
203{
204 const int requested = static_cast<int>(flags);
205 const int supported =
206 static_cast<int>(XrdCl::PrepareFlags::Stage)
207 | static_cast<int>(XrdCl::PrepareFlags::Cancel)
208 | static_cast<int>(XrdCl::PrepareFlags::Evict);
209
210 if(requested & ~supported)
211 {
212 return ErrorStatus(XrdCl::errNotSupported,
213 "HTTP Tape REST prepare supports stage, cancel, and evict only");
214 }
215
216 int operations = 0;
217 if(flags & XrdCl::PrepareFlags::Stage) ++operations;
218 if(flags & XrdCl::PrepareFlags::Cancel) ++operations;
219 if(flags & XrdCl::PrepareFlags::Evict) ++operations;
220
221 if(operations == 0)
222 {
223 return ErrorStatus(XrdCl::errNotSupported,
224 "HTTP Tape REST prepare supports stage, cancel, and evict only");
225 }
226 if(operations > 1)
227 {
228 return ErrorStatus(XrdCl::errInvalidArgs,
229 "HTTP Tape REST prepare expects exactly one operation flag");
230 }
231 return XrdCl::XRootDStatus();
232}
233
234XrdCl::XRootDStatus PrepareStageFiles(
235 const std::vector<std::string> &fileList,
236 std::vector<StageFileSpec> &files)
237{
238 if(fileList.empty())
239 {
240 return ErrorStatus(XrdCl::errInvalidArgs,
241 "stage requires at least one file");
242 }
243
244 files.clear();
245 files.reserve(fileList.size());
246 for(const auto &file : fileList)
247 {
248 if(file.compare(0, kStructuredStagePrefix.size(),
249 kStructuredStagePrefix) == 0)
250 {
251 const Json json = Json::parse(
252 file.substr(kStructuredStagePrefix.size()), nullptr, false);
253 if(json.is_discarded())
254 {
255 return ErrorStatus(XrdCl::errInvalidArgs,
256 "malformed structured tape stage entry");
257 }
258 if(!json.is_object())
259 {
260 return ErrorStatus(XrdCl::errInvalidArgs,
261 "structured tape stage entry must be a JSON object");
262 }
263
264 StageFileSpec entry;
265 if(json.contains("url"))
266 {
267 if(!json["url"].is_string())
268 {
269 return ErrorStatus(XrdCl::errInvalidArgs,
270 "structured tape stage entry url must be a string");
271 }
272 entry.url = json["url"].get<std::string>();
273 }
274 if(json.contains("path"))
275 {
276 if(!json["path"].is_string())
277 {
278 return ErrorStatus(XrdCl::errInvalidArgs,
279 "structured tape stage entry path must be a string");
280 }
281 entry.path = json["path"].get<std::string>();
282 }
283 if(entry.url.empty() && entry.path.empty())
284 {
285 return ErrorStatus(XrdCl::errInvalidArgs,
286 "structured tape stage entry requires url or path");
287 }
288 if(json.contains("diskLifetime"))
289 {
290 if(!json["diskLifetime"].is_string())
291 {
292 return ErrorStatus(XrdCl::errInvalidArgs,
293 "structured tape stage entry diskLifetime must be a string");
294 }
295 entry.diskLifetime = json["diskLifetime"].get<std::string>();
296 }
297 if(json.contains("targetedMetadata"))
298 {
299 if(!json["targetedMetadata"].is_object())
300 {
301 return ErrorStatus(XrdCl::errInvalidArgs,
302 "structured tape stage entry targetedMetadata must be a JSON object");
303 }
304 entry.targetedMetadata = json["targetedMetadata"];
305 }
306 files.push_back(entry);
307 continue;
308 }
309
310 StageFileSpec entry;
311 entry.url = file;
312 files.push_back(entry);
313 }
314 return XrdCl::XRootDStatus();
315}
316
317// Map an unexpected HTTP response code to an XRootDStatus, reusing the
318// plugin-wide HTTP-to-XRootD status conversion for error codes.
319XrdCl::XRootDStatus HttpErrorStatus(long statusCode,
320 const std::string &message)
321{
322 const unsigned status = statusCode < 0 ? 0 : statusCode;
324 {
325 return ErrorStatus(XrdCl::errErrorResponse, message);
326 }
327 const auto [code, errNo] = XrdClHttp::HTTPStatusConvert(status);
328 return XrdCl::XRootDStatus(XrdCl::stError, code, errNo, message);
329}
330
331bool PathFromInput(const std::string &input, std::string &path,
332 std::string &error)
333{
334 if(input.empty())
335 {
336 error = "empty path";
337 return false;
338 }
339 if(input.front() == '/')
340 {
341 path = NormalizeTapePath(input);
342 return true;
343 }
344
345 std::string endpoint;
346 return UrlEndpointAndPath(input, endpoint, path, error);
347}
348
349Json PathsRequestBody(const std::vector<std::string> &paths)
350{
351 Json body;
352 body["paths"] = paths;
353 return body;
354}
355
356void SetPathsPostRequest(XrdClHttp::TapeHttpRequest &request,
357 const std::string &endpoint,
358 const std::string &resource,
359 const std::vector<std::string> &paths)
360{
362 request.url = XrdOucUtils::JoinUrl(endpoint, resource);
363 request.body = PathsRequestBody(paths).dump();
364}
365
366XrdCl::XRootDStatus PathsFromInputs(const std::vector<std::string> &inputs,
367 std::vector<std::string> &paths)
368{
369 paths.clear();
370 paths.reserve(inputs.size());
371 for(const auto &input : inputs)
372 {
373 std::string path;
374 std::string error;
375 if(!PathFromInput(input, path, error))
376 {
377 return ErrorStatus(XrdCl::errInvalidArgs, error);
378 }
379 paths.push_back(path);
380 }
381 return XrdCl::XRootDStatus();
382}
383
384Json StageRequestBody(const std::vector<StageFileSpec> &files,
385 std::string &error)
386{
387 Json body;
388 body["files"] = Json::array();
389 for(const auto &file : files)
390 {
391 const std::string input = file.path.empty() ? file.url : file.path;
392 if(input.empty())
393 {
394 error = "stage file is missing both URL and path";
395 return Json();
396 }
397
398 std::string path;
399 if(!PathFromInput(input, path, error)) return Json();
400
401 Json item;
402 item["path"] = path;
403 if(!file.diskLifetime.empty()) item["diskLifetime"] = file.diskLifetime;
404 if(!file.targetedMetadata.is_null())
405 item["targetedMetadata"] = file.targetedMetadata;
406 body["files"].push_back(item);
407 }
408 return body;
409}
410
411bool CopyOptionalTimestamp(const Json &input, const char *key,
412 Json &output, bool &present)
413{
414 present = false;
415 const auto item = input.find(key);
416 if(item == input.end()) return true;
417 if(!item->is_number_integer()) return false;
418
419 std::uint64_t timestamp = 0;
420 if(item->is_number_unsigned())
421 {
422 timestamp = item->get<std::uint64_t>();
423 }
424 else
425 {
426 const std::int64_t signedTimestamp = item->get<std::int64_t>();
427 if(signedTimestamp < 0) return false;
428 timestamp = static_cast<std::uint64_t>(signedTimestamp);
429 }
430 if(timestamp > static_cast<std::uint64_t>(
431 std::numeric_limits<std::time_t>::max()))
432 {
433 return false;
434 }
435
436 output[key] = static_cast<std::time_t>(timestamp);
437 present = true;
438 return true;
439}
440
441XrdCl::XRootDStatus NormalizeStageFileStatusJson(
442 const Json &input, Json &output)
443{
444 output = Json::object();
445 const auto pathItem = input.find("path");
446 if(pathItem == input.end() || !pathItem->is_string()
447 || pathItem->get_ref<const std::string &>().empty())
448 {
449 return ErrorStatus(XrdCl::errInvalidResponse,
450 "stage request response contains a file entry without a non-empty "
451 "string path");
452 }
453 const auto &path = pathItem->get_ref<const std::string &>();
454 output["path"] = NormalizeTapePath(path);
455 const bool hasOnDisk = input.contains("onDisk");
456 const bool hasState = input.contains("state");
457 if(hasOnDisk && !input["onDisk"].is_boolean())
458 {
459 return ErrorStatus(XrdCl::errInvalidResponse,
460 "stage request response contains a non-boolean onDisk field");
461 }
462 if(hasState && !input["state"].is_string())
463 {
464 return ErrorStatus(XrdCl::errInvalidResponse,
465 "stage request response contains a non-string state field");
466 }
467 if(hasOnDisk && hasState)
468 {
469 return ErrorStatus(XrdCl::errInvalidResponse,
470 "stage request response contains both onDisk and state");
471 }
472 if(hasOnDisk)
473 {
474 output["onDisk"] = input["onDisk"];
475 }
476 if(hasState && !input["state"].get_ref<const std::string &>().empty())
477 {
478 output["state"] = input["state"];
479 }
480 const auto errorItem = input.find("error");
481 if(errorItem != input.end())
482 {
483 if(!errorItem->is_string())
484 {
485 return ErrorStatus(XrdCl::errInvalidResponse,
486 "stage request response contains a non-string error field");
487 }
488 const auto &error = errorItem->get_ref<const std::string &>();
489 if(!error.empty()) output["error"] = error;
490 }
491 bool hasStartedAt = false;
492 bool hasFinishedAt = false;
493 if(!CopyOptionalTimestamp(input, "startedAt", output, hasStartedAt)
494 || !CopyOptionalTimestamp(input, "finishedAt", output, hasFinishedAt))
495 {
496 return ErrorStatus(XrdCl::errInvalidResponse,
497 "stage request response contains a non-integer file timestamp");
498 }
499 if(!hasState && (hasStartedAt || hasFinishedAt))
500 {
501 return ErrorStatus(XrdCl::errInvalidResponse,
502 "stage request response contains file timestamps without state");
503 }
504 return XrdCl::XRootDStatus();
505}
506
507XrdCl::XRootDStatus NormalizeStageStatusJson(const Json &input,
508 Json &output)
509{
510 output = Json::object();
511 if(!input.is_object())
512 {
513 return ErrorStatus(XrdCl::errInvalidResponse,
514 "stage request response is not a JSON object");
515 }
516 if(!input.contains("id") || !input["id"].is_string())
517 {
518 return ErrorStatus(XrdCl::errInvalidResponse,
519 "stage request response does not contain a string id");
520 }
521 if(input["id"].get_ref<const std::string &>().empty())
522 {
523 return ErrorStatus(XrdCl::errInvalidResponse,
524 "stage request response contains an empty id");
525 }
526 if(!input.contains("files") || !input["files"].is_array())
527 {
528 return ErrorStatus(XrdCl::errInvalidResponse,
529 "stage request response does not contain a files array");
530 }
531
532 output["id"] = input["id"];
533 bool hasCreatedAt = false;
534 bool hasStartedAt = false;
535 bool hasCompletedAt = false;
536 if(!CopyOptionalTimestamp(input, "createdAt", output, hasCreatedAt)
537 || !CopyOptionalTimestamp(input, "startedAt", output, hasStartedAt)
538 || !CopyOptionalTimestamp(input, "completedAt", output,
539 hasCompletedAt))
540 {
541 return ErrorStatus(XrdCl::errInvalidResponse,
542 "stage request response contains a non-integer timestamp");
543 }
544 if(!hasCreatedAt || !hasStartedAt)
545 {
546 return ErrorStatus(XrdCl::errInvalidResponse,
547 "stage request response is missing createdAt or startedAt");
548 }
549
550 output["files"] = Json::array();
551 for(const auto &file : input["files"])
552 {
553 if(!file.is_object())
554 {
555 return ErrorStatus(XrdCl::errInvalidResponse,
556 "stage request response contains a non-object file entry");
557 }
558 Json normalizedFile;
559 XrdCl::XRootDStatus fileStatusResult =
560 NormalizeStageFileStatusJson(file, normalizedFile);
561 if(!fileStatusResult.IsOK()) return fileStatusResult;
562 output["files"].push_back(std::move(normalizedFile));
563 }
564 return XrdCl::XRootDStatus();
565}
566
567XrdCl::XRootDStatus UnexpectedStatus(long statusCode,
568 const std::string &body,
569 const std::string &operation)
570{
571 return HttpErrorStatus(statusCode,
572 operation + " failed: " + FormatProblemResponse(statusCode, body));
573}
574
575XrdCl::XRootDStatus EmptyResponseStatus(long statusCode,
576 const std::string &body,
577 const std::string &operation)
578{
579 if(statusCode < 200 || statusCode >= 300)
580 {
581 return UnexpectedStatus(statusCode, body, operation);
582 }
583 return XrdCl::XRootDStatus();
584}
585
586std::string CanonicalLocality(const std::string &locality)
587{
588 for(const auto name : kTapeLocalities)
589 {
590 if(strcasecmp(locality.c_str(), name.data()) == 0)
591 return std::string(name);
592 }
593 return "UNKNOWN";
594}
595
596Json ArchiveInfoToJson(const Json *item, const std::string &url,
597 const std::string &path)
598{
599 Json result;
600 result["url"] = url;
601 result["path"] = path;
602
603 if(!item)
604 {
605 result["error"] = "missing response item for path=" + path;
606 return result;
607 }
608
609 if(item->contains("error"))
610 {
611 if((*item)["error"].is_string())
612 {
613 const std::string error = (*item)["error"].get<std::string>();
614 if(error.empty()) result["locality"] = "UNKNOWN";
615 else result["error"] = error;
616 }
617 else
618 {
619 result["error"] = "error field is not a string";
620 }
621 return result;
622 }
623
624 const auto localityItem = item->find("locality");
625 if(localityItem == item->end() || !localityItem->is_string()
626 || localityItem->get_ref<const std::string &>().empty())
627 {
628 result["error"] = "locality attribute missing";
629 return result;
630 }
631
632 const auto &locality = localityItem->get_ref<const std::string &>();
633 result["locality"] = CanonicalLocality(locality);
634 return result;
635}
636
637const Json *FindArchiveInfoItem(const Json &response, const std::string &path)
638{
639 if(!response.is_array()) return nullptr;
640
641 for(const auto &item : response)
642 {
643 if(!item.is_object() || !item.contains("path") || !item["path"].is_string())
644 {
645 continue;
646 }
647 if(NormalizeTapePath(item["path"].get<std::string>()) == path)
648 {
649 return &item;
650 }
651 }
652 return nullptr;
653}
654
655}
656
657namespace XrdClHttp
658{
660 {
661 enum class Kind
662 {
663 Invalid,
664 Discover,
665 Stage,
666 StageStatus,
667 StageCancel,
668 StageDelete,
669 Release,
670 ArchiveInfo
671 };
672
673 enum class Phase
674 {
675 Initial,
676 Discovery,
677 Request,
678 Complete
679 };
680
681 std::string storageUrl;
685 std::vector<StageFileSpec> stageFiles;
686 std::vector<std::string> urls;
687 std::vector<std::string> paths;
688 std::string requestId;
689 };
690
691 namespace
692 {
693 XrdCl::XRootDStatus ParseDiscoveryResponse(const std::string &body,
694 TapeEndpoint &endpoint)
695 {
696 Json json;
697 XrdCl::XRootDStatus status =
698 ParseJsonResponse(body, "discovery", json);
699 if(!status.IsOK()) return status;
700
701 if(!json.contains("sitename") || !json["sitename"].is_string())
702 {
703 return ErrorStatus(XrdCl::errInvalidResponse,
704 "discovery response does not contain a string sitename");
705 }
706 if(!json.contains("endpoints") || !json["endpoints"].is_array())
707 {
708 return ErrorStatus(XrdCl::errInvalidResponse,
709 "discovery response does not contain an endpoints array");
710 }
711
712 TapeEndpoint selected;
713 int selectedVersion = -1;
714 for(const auto &candidate : json["endpoints"])
715 {
716 if(!candidate.is_object()
717 || !candidate.contains("uri") || !candidate["uri"].is_string()
718 || !candidate.contains("version")
719 || !candidate["version"].is_string())
720 {
721 continue;
722 }
723
724 const std::string version = candidate["version"].get<std::string>();
725 std::string_view versionNumber(version);
726 if(!versionNumber.empty()
727 && (versionNumber.front() == 'v' || versionNumber.front() == 'V'))
728 {
729 versionNumber.remove_prefix(1);
730 }
731
732 int parsedVersion = -1;
733 try
734 {
735 parsedVersion = XrdOucUtils::touint8_t(versionNumber);
736 }
737 catch(const std::invalid_argument &)
738 {
739 continue;
740 }
741 catch(const std::out_of_range &)
742 {
743 continue;
744 }
745 if(parsedVersion > 1 || parsedVersion < selectedVersion)
746 {
747 continue;
748 }
749
750 const std::string uri = candidate["uri"].get<std::string>();
751 if(!IsHttpEndpointUri(uri)) continue;
752
753 selectedVersion = parsedVersion;
754 selected.uri = uri;
755 selected.version = version;
756 }
757
758 if(selected.uri.empty())
759 {
760 return ErrorStatus(XrdCl::errNotSupported,
761 "discovery response does not advertise a supported v0/v1 endpoint");
762 }
763
764 selected.sitename = json["sitename"].get<std::string>();
765 endpoint = selected;
766 return XrdCl::XRootDStatus();
767 }
768
769 XrdCl::XRootDStatus ValidateArchiveUrls(
770 const std::vector<std::string> &urls,
771 std::vector<std::string> &paths)
772 {
773 if(urls.empty())
774 {
775 return ErrorStatus(XrdCl::errInvalidArgs, "missing URL");
776 }
777
778 std::string firstEndpoint;
779 paths.clear();
780 paths.reserve(urls.size());
781 for(const auto &url : urls)
782 {
783 std::string endpoint;
784 std::string path;
785 std::string error;
786 if(!UrlEndpointAndPath(url, endpoint, path, error))
787 {
788 return ErrorStatus(XrdCl::errInvalidArgs, error);
789 }
790 if(firstEndpoint.empty()) firstEndpoint = endpoint;
791 else if(endpoint != firstEndpoint)
792 {
793 return ErrorStatus(XrdCl::errInvalidArgs,
794 "archiveinfo URLs must belong to the same storage endpoint");
795 }
796 paths.push_back(path);
797 }
798 return XrdCl::XRootDStatus();
799 }
800
801 XrdCl::XRootDStatus ParseArchiveInfoResponse(
802 const std::string &body,
803 const std::vector<std::string> &urls,
804 const std::vector<std::string> &paths,
805 std::string &result)
806 {
807 Json json;
808 XrdCl::XRootDStatus status =
809 ParseJsonResponse(body, "archiveinfo", json);
810 if(!status.IsOK()) return status;
811 if(!json.is_array())
812 {
813 return ErrorStatus(XrdCl::errInvalidResponse,
814 "archiveinfo response is not a JSON array");
815 }
816
817 Json response = Json::array();
818 for(std::size_t index = 0; index < paths.size(); ++index)
819 {
820 response.push_back(ArchiveInfoToJson(
821 FindArchiveInfoItem(json, paths[index]), urls[index], paths[index]));
822 }
823 result = response.dump();
824 return XrdCl::XRootDStatus();
825 }
826
827 }
828
830 const std::string &url,
831 const std::vector<std::string> &fileList,
833 pImpl(std::make_unique<Impl>())
834 {
835 pImpl->storageUrl = url;
836 pImpl->initialStatus = ValidateTapePrepareFlags(flags);
837 if(!pImpl->initialStatus.IsOK()) return;
838
840 {
841 pImpl->kind = Impl::Kind::Stage;
842 pImpl->initialStatus = PrepareStageFiles(fileList, pImpl->stageFiles);
843 return;
844 }
845
846 if(fileList.size() < 2)
847 {
848 pImpl->initialStatus = ErrorStatus(XrdCl::errInvalidArgs,
849 "cancel and evict require a request id and at least one path");
850 return;
851 }
852
853 pImpl->requestId = fileList.front();
854 if(!IsValidRequestId(pImpl->requestId))
855 {
856 pImpl->initialStatus = ErrorStatus(XrdCl::errInvalidArgs,
857 "cancel and evict require a non-empty request id");
858 return;
859 }
860 pImpl->initialStatus = PathsFromInputs(
861 std::vector<std::string>(fileList.begin() + 1, fileList.end()),
862 pImpl->paths);
863 if(!pImpl->initialStatus.IsOK()) return;
864 pImpl->kind = (flags & XrdCl::PrepareFlags::Cancel)
866 }
867
869 const std::string &url,
870 XrdCl::QueryCode::Code queryCode,
871 const XrdCl::Buffer &arg ):
872 pImpl(std::make_unique<Impl>())
873 {
874 pImpl->storageUrl = url;
875 std::vector<std::string> args;
876 XrdCl::Utils::splitString(args, arg.ToString(), "\n");
877 if(queryCode == XrdCl::QueryCode::Prepare)
878 {
879 if(args.size() != 1 || !IsValidRequestId(args.front()))
880 {
881 pImpl->initialStatus = ErrorStatus(XrdCl::errInvalidArgs,
882 "prepare query expects a single request id");
883 return;
884 }
885 pImpl->kind = Impl::Kind::StageStatus;
886 pImpl->requestId = args.front();
887 return;
888 }
889
890 if(queryCode != XrdCl::QueryCode::Opaque)
891 {
892 pImpl->initialStatus = ErrorStatus(XrdCl::errNotSupported,
893 "unsupported HTTP query");
894 return;
895 }
896 if(args.empty() || args.front().find('\r') != std::string::npos)
897 {
898 pImpl->initialStatus = ErrorStatus(XrdCl::errInvalidArgs,
899 "missing opaque query command");
900 return;
901 }
902
903 if(args.front() == "tape.discover")
904 {
905 pImpl->kind = Impl::Kind::Discover;
906 return;
907 }
908 if(args.front() == "tape.stage_delete")
909 {
910 if(args.size() != 2 || !IsValidRequestId(args[1]))
911 {
912 pImpl->initialStatus = ErrorStatus(XrdCl::errInvalidArgs,
913 "tape.stage_delete expects a request id");
914 return;
915 }
916 pImpl->kind = Impl::Kind::StageDelete;
917 pImpl->requestId = args[1];
918 return;
919 }
920 if(args.front() == "tape.archiveinfo")
921 {
922 if(args.size() < 2)
923 {
924 pImpl->initialStatus = ErrorStatus(XrdCl::errInvalidArgs,
925 "tape.archiveinfo expects non-empty URLs");
926 return;
927 }
928 pImpl->urls.assign(args.begin() + 1, args.end());
929 for(const auto &archiveUrl : pImpl->urls)
930 {
931 if(archiveUrl.find('\r') != std::string::npos)
932 {
933 pImpl->initialStatus = ErrorStatus(XrdCl::errInvalidArgs,
934 "tape.archiveinfo expects non-empty URLs");
935 return;
936 }
937 }
938 pImpl->initialStatus = ValidateArchiveUrls(pImpl->urls, pImpl->paths);
939 if(pImpl->initialStatus.IsOK()) pImpl->kind = Impl::Kind::ArchiveInfo;
940 return;
941 }
942
943 pImpl->initialStatus = ErrorStatus(XrdCl::errNotSupported,
944 "unsupported HTTP opaque query");
945 }
946
948
950 {
951 if(!pImpl->initialStatus.IsOK()) return pImpl->initialStatus;
952 if(pImpl->kind == Impl::Kind::Invalid)
953 {
954 return ErrorStatus(XrdCl::errNotSupported,
955 "unsupported HTTP Tape REST operation");
956 }
957
958 std::string endpoint;
959 std::string path;
960 std::string error;
961 if(!UrlEndpointAndPath(pImpl->storageUrl, endpoint, path, error))
962 {
963 return ErrorStatus(XrdCl::errInvalidArgs, error);
964 }
965
966 request = TapeHttpRequest();
967 request.url = XrdOucUtils::JoinUrl(
968 endpoint, "/.well-known/wlcg-tape-rest-api");
969 pImpl->phase = Impl::Phase::Discovery;
970 return XrdCl::XRootDStatus();
971 }
972
974 CURL *curl,
975 long statusCode,
976 const std::string &body,
977 TapeHttpRequest &request,
978 std::string &response,
979 bool &complete )
980 {
981 complete = false;
982 response.clear();
983
984 if(pImpl->phase == Impl::Phase::Discovery)
985 {
986 if(statusCode != 200)
987 {
988 complete = true;
989 return UnexpectedStatus(statusCode, body, "Tape REST discovery");
990 }
991
992 TapeEndpoint endpoint;
993 XrdCl::XRootDStatus status = ParseDiscoveryResponse(body, endpoint);
994 if(!status.IsOK())
995 {
996 complete = true;
997 return status;
998 }
999 if(pImpl->kind == Impl::Kind::Discover)
1000 {
1001 Json result;
1002 result["uri"] = endpoint.uri;
1003 result["version"] = endpoint.version;
1004 result["sitename"] = endpoint.sitename;
1005 response = result.dump();
1006 complete = true;
1007 pImpl->phase = Impl::Phase::Complete;
1008 return XrdCl::XRootDStatus();
1009 }
1010
1011 request = TapeHttpRequest();
1012 const std::string encodedRequestId = pImpl->requestId.empty()
1013 ? "" : PercentEncodeUrlPathSegment(pImpl->requestId, curl);
1014 switch(pImpl->kind)
1015 {
1016 case Impl::Kind::Stage:
1017 {
1018 std::string error;
1019 Json stageBody = StageRequestBody(pImpl->stageFiles, error);
1020 if(!error.empty())
1021 {
1022 complete = true;
1023 return ErrorStatus(XrdCl::errInvalidArgs, error);
1024 }
1025 request.method = HttpVerb::POST;
1026 request.url = XrdOucUtils::JoinUrl(endpoint.uri, "/stage");
1027 request.body = stageBody.dump();
1028 break;
1029 }
1031 request.url = XrdOucUtils::JoinUrl(
1032 endpoint.uri, "/stage/" + encodedRequestId);
1033 break;
1035 SetPathsPostRequest(request, endpoint.uri,
1036 "/stage/" + encodedRequestId + "/cancel", pImpl->paths);
1037 break;
1039 request.method = HttpVerb::DELETE;
1040 request.url = XrdOucUtils::JoinUrl(
1041 endpoint.uri, "/stage/" + encodedRequestId);
1042 break;
1044 SetPathsPostRequest(request, endpoint.uri,
1045 "/release/" + encodedRequestId, pImpl->paths);
1046 break;
1048 SetPathsPostRequest(request, endpoint.uri, "/archiveinfo",
1049 pImpl->paths);
1050 break;
1053 complete = true;
1054 return ErrorStatus(XrdCl::errInternal,
1055 "invalid Tape REST operation state");
1056 }
1057 pImpl->phase = Impl::Phase::Request;
1058 return XrdCl::XRootDStatus();
1059 }
1060
1061 if(pImpl->phase != Impl::Phase::Request)
1062 {
1063 complete = true;
1064 return ErrorStatus(XrdCl::errInternal,
1065 "invalid Tape REST response state");
1066 }
1067
1068 complete = true;
1069 pImpl->phase = Impl::Phase::Complete;
1070 switch(pImpl->kind)
1071 {
1072 case Impl::Kind::Stage:
1073 {
1074 if(statusCode != 201)
1075 return UnexpectedStatus(statusCode, body, "stage request submission");
1076 Json json;
1077 XrdCl::XRootDStatus status =
1078 ParseJsonResponse(body, "stage submission", json);
1079 if(!status.IsOK()) return status;
1080 if(!json.contains("requestId") || !json["requestId"].is_string()
1081 || json["requestId"].get_ref<const std::string &>().empty())
1082 {
1083 return ErrorStatus(XrdCl::errInvalidResponse,
1084 "stage submission response does not contain a non-empty string "
1085 "requestId");
1086 }
1087 response = json["requestId"].get<std::string>();
1088 return XrdCl::XRootDStatus();
1089 }
1091 {
1092 if(statusCode != 200)
1093 return UnexpectedStatus(statusCode, body, "stage request polling");
1094 Json json;
1095 XrdCl::XRootDStatus status =
1096 ParseJsonResponse(body, "stage polling", json);
1097 if(!status.IsOK()) return status;
1098 Json normalizedStatus;
1099 status = NormalizeStageStatusJson(json, normalizedStatus);
1100 if(status.IsOK()) response = normalizedStatus.dump();
1101 return status;
1102 }
1104 return EmptyResponseStatus(statusCode, body,
1105 "stage request cancellation");
1107 return EmptyResponseStatus(statusCode, body, "stage request deletion");
1109 return EmptyResponseStatus(statusCode, body, "stage request release");
1111 if(statusCode != 200)
1112 return UnexpectedStatus(statusCode, body, "archiveinfo call");
1113 return ParseArchiveInfoResponse(body, pImpl->urls, pImpl->paths,
1114 response);
1117 return ErrorStatus(XrdCl::errInternal,
1118 "invalid Tape REST operation state");
1119 }
1120 return ErrorStatus(XrdCl::errInternal, "invalid Tape REST operation");
1121 }
1122
1123 std::string TapeProblemResponse(long statusCode, const std::string &body)
1124 {
1125 return FormatProblemResponse(statusCode, body);
1126 }
1127}
void CURL
nlohmann::json json
XrdCl::XRootDStatus Advance(CURL *curl, long statusCode, const std::string &body, TapeHttpRequest &request, std::string &response, bool &complete)
TapeOperation(const std::string &url, const std::vector< std::string > &fileList, XrdCl::PrepareFlags::Flags flags)
XrdCl::XRootDStatus Start(TapeHttpRequest &request)
Binary blob representation.
std::string ToString() const
Convert the buffer to a string.
URL representation.
Definition XrdClURL.hh:31
static void splitString(Container &result, const std::string &input, const std::string &delimiter)
Split a string.
Definition XrdClUtils.hh:56
static uint8_t touint8_t(const std::string_view sv)
static std::string NormalizePath(const std::string &path)
static std::string JoinUrl(const std::string &base, const std::string &path)
std::pair< uint16_t, uint32_t > HTTPStatusConvert(unsigned status)
bool HTTPStatusIsError(unsigned status)
std::string TapeProblemResponse(long statusCode, const std::string &body)
const uint16_t errErrorResponse
const uint16_t errNotImplemented
Operation is not implemented.
const uint16_t stError
An error occurred that could potentially be retried.
const uint16_t errInternal
Internal error.
const uint16_t errInvalidResponse
const uint16_t errInvalidArgs
const uint16_t errNotSupported
std::vector< std::string > paths
XrdCl::XRootDStatus initialStatus
std::vector< std::string > urls
std::vector< StageFileSpec > stageFiles
@ Cancel
cancel staging request
Code
XRootD query request codes.
@ Opaque
Implementation dependent.
@ Prepare
Query prepare status.
bool IsOK() const
We're fine.