XRootD
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 
29 #include "XrdCl/XrdClConstants.hh"
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 
53 namespace
54 {
55 using Json = nlohmann::json;
56 
57 constexpr std::array<std::string_view, 6> kTapeLocalities = {{
58  "DISK", "TAPE", "DISK_AND_TAPE", "LOST", "NONE", "UNAVAILABLE"
59 }};
60 
61 struct TapeEndpoint
62 {
63  std::string uri;
64  std::string version;
65  std::string sitename;
66 };
67 
68 struct StageFileSpec
69 {
70  std::string url;
71  std::string path;
72  std::string diskLifetime;
73  Json targetedMetadata;
74 };
75 
76 std::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 
83 std::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 
95 bool 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 
105 bool 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 
136 std::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 
167 XrdCl::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 
182 XrdCl::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 
193 const std::string kStructuredStagePrefix = "xrdclhttp.tape.stage:";
194 
195 bool IsValidRequestId(const std::string &requestId)
196 {
197  return !requestId.empty()
198  && requestId.find_first_of("\r\n") == std::string::npos;
199 }
200 
201 XrdCl::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 
234 XrdCl::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.
319 XrdCl::XRootDStatus HttpErrorStatus(long statusCode,
320  const std::string &message)
321 {
322  const unsigned status = statusCode < 0 ? 0 : statusCode;
323  if(!XrdClHttp::HTTPStatusIsError(status))
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 
331 bool 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 
349 Json PathsRequestBody(const std::vector<std::string> &paths)
350 {
351  Json body;
352  body["paths"] = paths;
353  return body;
354 }
355 
356 void SetPathsPostRequest(XrdClHttp::TapeHttpRequest &request,
357  const std::string &endpoint,
358  const std::string &resource,
359  const std::vector<std::string> &paths)
360 {
361  request.method = XrdClHttp::HttpVerb::POST;
362  request.url = XrdOucUtils::JoinUrl(endpoint, resource);
363  request.body = PathsRequestBody(paths).dump();
364 }
365 
366 XrdCl::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 
384 Json 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 
411 bool 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 
441 XrdCl::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 
507 XrdCl::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 
567 XrdCl::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 
575 XrdCl::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 
586 std::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 
596 Json 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 
637 const 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 
657 namespace 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;
682  Kind kind = Kind::Invalid;
683  Phase phase = Phase::Initial;
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 
839  if(flags & XrdCl::PrepareFlags::Stage)
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)
865  ? Impl::Kind::StageCancel : Impl::Kind::Release;
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 
947  TapeOperation::~TapeOperation() = default;
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  }
1030  case Impl::Kind::StageStatus:
1031  request.url = XrdOucUtils::JoinUrl(
1032  endpoint.uri, "/stage/" + encodedRequestId);
1033  break;
1034  case Impl::Kind::StageCancel:
1035  SetPathsPostRequest(request, endpoint.uri,
1036  "/stage/" + encodedRequestId + "/cancel", pImpl->paths);
1037  break;
1038  case Impl::Kind::StageDelete:
1039  request.method = HttpVerb::DELETE;
1040  request.url = XrdOucUtils::JoinUrl(
1041  endpoint.uri, "/stage/" + encodedRequestId);
1042  break;
1043  case Impl::Kind::Release:
1044  SetPathsPostRequest(request, endpoint.uri,
1045  "/release/" + encodedRequestId, pImpl->paths);
1046  break;
1047  case Impl::Kind::ArchiveInfo:
1048  SetPathsPostRequest(request, endpoint.uri, "/archiveinfo",
1049  pImpl->paths);
1050  break;
1051  case Impl::Kind::Invalid:
1052  case Impl::Kind::Discover:
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  }
1090  case Impl::Kind::StageStatus:
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  }
1103  case Impl::Kind::StageCancel:
1104  return EmptyResponseStatus(statusCode, body,
1105  "stage request cancellation");
1106  case Impl::Kind::StageDelete:
1107  return EmptyResponseStatus(statusCode, body, "stage request deletion");
1108  case Impl::Kind::Release:
1109  return EmptyResponseStatus(statusCode, body, "stage request release");
1110  case Impl::Kind::ArchiveInfo:
1111  if(statusCode != 200)
1112  return UnexpectedStatus(statusCode, body, "archiveinfo call");
1113  return ParseArchiveInfoResponse(body, pImpl->urls, pImpl->paths,
1114  response);
1115  case Impl::Kind::Invalid:
1116  case Impl::Kind::Discover:
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.
Definition: XrdClBuffer.hh:34
std::string ToString() const
Convert the buffer to a string.
Definition: XrdClBuffer.hh:215
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
Definition: XrdClStatus.hh:105
const uint16_t errNotImplemented
Operation is not implemented.
Definition: XrdClStatus.hh:64
const uint16_t stError
An error occurred that could potentially be retried.
Definition: XrdClStatus.hh:32
const uint16_t errInternal
Internal error.
Definition: XrdClStatus.hh:56
const uint16_t errInvalidResponse
Definition: XrdClStatus.hh:99
const uint16_t errInvalidArgs
Definition: XrdClStatus.hh:58
const uint16_t errNotSupported
Definition: XrdClStatus.hh:62
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.
Definition: XrdClStatus.hh:124