XRootD
Loading...
Searching...
No Matches
XrdHttpTapeApiStore.cc
Go to the documentation of this file.
1/******************************************************************************/
2/* */
3/* X r d H t t p T a p e A p i S t o r e . c c */
4/* */
5/* (c) 2026 by the XRootD Collaboration */
6/* */
7/* This file is part of the XRootD software suite. */
8/* */
9/******************************************************************************/
10
12
13#include "XrdOuc/XrdOucUtils.hh"
14
15#include <cctype>
16#include <chrono>
17#include <fstream>
18#include <set>
19#include <system_error>
20#include <uuid/uuid.h>
21
22namespace
23{
24using Json = nlohmann::json;
25namespace fs = std::filesystem;
26
27XrdHttpTapeApiStore::Status Error(int code, const std::string &message)
28{
29 return {code, message};
30}
31
32bool IsWithin(const fs::path &root, const fs::path &path)
33{
34 auto rootPart = root.begin();
35 auto pathPart = path.begin();
36 while(rootPart != root.end() && pathPart != path.end())
37 {
38 if(*rootPart != *pathPart) return false;
39 ++rootPart;
40 ++pathPart;
41 }
42 return rootPart == root.end();
43}
44
45bool IsRegularFile(const fs::path &path, bool &regular)
46{
47 std::error_code error;
48 const fs::file_status status = fs::status(path, error);
49 if(error == std::errc::no_such_file_or_directory)
50 {
51 regular = false;
52 return true;
53 }
54 if(error) return false;
55 regular = fs::is_regular_file(status);
56 return true;
57}
58}
59
61 : m_root(std::move(root)),
62 m_archiveRoot(m_root / "archive"),
63 m_diskRoot(m_root / "disk"),
64 m_requestsRoot(m_root / "requests")
65{
66}
67
69{
70 std::error_code error;
71 fs::create_directories(m_archiveRoot, error);
72 if(error) return Error(500, "could not create the tape archive directory");
73 fs::create_directories(m_diskRoot, error);
74 if(error) return Error(500, "could not create the disk directory");
75 fs::create_directories(m_requestsRoot, error);
76 if(error) return Error(500, "could not create the request directory");
77
78 m_root = fs::weakly_canonical(m_root, error);
79 if(error) return Error(500, "could not resolve the Tape API directory");
80 m_archiveRoot = m_root / "archive";
81 m_diskRoot = m_root / "disk";
82 m_requestsRoot = m_root / "requests";
83 return {};
84}
85
86std::uint64_t XrdHttpTapeApiStore::Now()
87{
88 const auto now = std::chrono::system_clock::now().time_since_epoch();
89 return static_cast<std::uint64_t>(
90 std::chrono::duration_cast<std::chrono::seconds>(now).count());
91}
92
93std::string XrdHttpTapeApiStore::GenerateRequestId()
94{
95 uuid_t value;
96 uuid_generate_random(value);
97 char text[37] = {};
98 uuid_unparse_lower(value, text);
99 return text;
100}
101
102bool XrdHttpTapeApiStore::IsRequestId(const std::string &requestId)
103{
104 if(requestId.size() != 36) return false;
105 for(std::size_t index = 0; index < requestId.size(); ++index)
106 {
107 if(index == 8 || index == 13 || index == 18 || index == 23)
108 {
109 if(requestId[index] != '-') return false;
110 }
111 else if(!std::isxdigit(static_cast<unsigned char>(requestId[index])))
112 {
113 return false;
114 }
115 }
116 return true;
117}
118
119XrdHttpTapeApiStore::Status XrdHttpTapeApiStore::ResolvePath(
120 const fs::path &root, const std::string &path, std::string &normalized,
121 fs::path &resolved) const
122{
123 normalized = XrdOucUtils::NormalizePath(path);
124 if(normalized.empty() || normalized.front() != '/')
125 {
126 return Error(400, "file paths must be absolute");
127 }
128
129 const fs::path relative = fs::path(normalized).relative_path();
130 for(const auto &part : relative)
131 {
132 if(part == "." || part == "..")
133 {
134 return Error(400, "file paths must not contain traversal components");
135 }
136 }
137
138 std::error_code error;
139 resolved = fs::weakly_canonical(root / relative, error);
140 if(error || !IsWithin(root, resolved))
141 {
142 return Error(400, "file path is outside the configured storage root");
143 }
144 return {};
145}
146
147XrdHttpTapeApiStore::Status XrdHttpTapeApiStore::WriteRequest(
148 const Json &request) const
149{
150 const std::string requestId = request.at("id").get<std::string>();
151 const fs::path destination = m_requestsRoot / (requestId + ".json");
152 const fs::path temporary = m_requestsRoot / (requestId + ".json.tmp");
153
154 std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
155 if(!output) return Error(500, "could not write the stage request");
156 output << request.dump();
157 output.close();
158 if(!output)
159 {
160 std::error_code ignored;
161 fs::remove(temporary, ignored);
162 return Error(500, "could not write the stage request");
163 }
164
165 std::error_code error;
166 fs::rename(temporary, destination, error);
167 if(error)
168 {
169 fs::remove(temporary, error);
170 return Error(500, "could not persist the stage request");
171 }
172 return {};
173}
174
175XrdHttpTapeApiStore::Status XrdHttpTapeApiStore::ReadRequest(
176 const std::string &requestId, Json &request) const
177{
178 if(!IsRequestId(requestId)) return Error(404, "unknown stage request");
179
180 std::ifstream input(m_requestsRoot / (requestId + ".json"),
181 std::ios::binary);
182 if(!input) return Error(404, "unknown stage request");
183 try
184 {
185 input >> request;
186 }
187 catch(const std::exception &)
188 {
189 return Error(500, "could not read the stage request");
190 }
191 return {};
192}
193
194Json XrdHttpTapeApiStore::StatusResponse(const Json &request)
195{
196 Json response = {
197 {"id", request.at("id")},
198 {"createdAt", request.at("createdAt")},
199 {"startedAt", request.at("startedAt")},
200 {"files", Json::array()}
201 };
202 if(request.contains("completedAt"))
203 {
204 response["completedAt"] = request["completedAt"];
205 }
206
207 for(const auto &stored : request.at("files"))
208 {
209 Json file = {{"path", stored.at("path")}, {"state", stored.at("state")}};
210 for(const char *field : {"startedAt", "finishedAt", "error"})
211 {
212 if(stored.contains(field)) file[field] = stored[field];
213 }
214 response["files"].push_back(std::move(file));
215 }
216 return response;
217}
218
220 const Json &files, std::string &requestId)
221{
222 std::lock_guard<std::mutex> lock(m_mutex);
223 requestId = GenerateRequestId();
224 const std::uint64_t createdAt = Now();
225 Json request = {
226 {"id", requestId},
227 {"createdAt", createdAt},
228 {"startedAt", createdAt},
229 {"files", Json::array()}
230 };
231
232 for(const auto &input : files)
233 {
234 Json file = input;
235 std::string normalized;
236 fs::path archivePath;
237 Status status = ResolvePath(m_archiveRoot,
238 input.at("path").get<std::string>(),
239 normalized, archivePath);
240 file["path"] = normalized;
241 file["startedAt"] = createdAt;
242
243 fs::path diskPath;
244 if(status) status = ResolvePath(m_diskRoot, normalized,
245 normalized, diskPath);
246 bool archived = false;
247 if(status && !IsRegularFile(archivePath, archived))
248 {
249 status = Error(500, "could not inspect the archived file");
250 }
251 std::error_code error;
252 if(status && archived && fs::file_size(archivePath, error) == 0)
253 {
254 status = Error(400, "zero-length files cannot be staged from tape");
255 }
256 if(error) status = Error(500, "could not inspect the archived file");
257
258 bool onDisk = false;
259 if(status && !IsRegularFile(diskPath, onDisk))
260 {
261 status = Error(500, "could not inspect the disk file");
262 }
263 if(status && archived && !onDisk)
264 {
265 error.clear();
266 fs::create_directories(diskPath.parent_path(), error);
267 if(!error)
268 {
269 fs::copy_file(archivePath, diskPath, fs::copy_options::overwrite_existing,
270 error);
271 }
272 if(error) status = Error(500, "could not stage the archived file");
273 }
274 file["finishedAt"] = Now();
275 if(status && archived)
276 {
277 file["state"] = "COMPLETED";
278 }
279 else
280 {
281 file["state"] = "FAILED";
282 file["error"] = status ? "file is not stored on tape" : status.message;
283 }
284 request["files"].push_back(std::move(file));
285 }
286
287 request["completedAt"] = Now();
288 return WriteRequest(request);
289}
290
292 const std::string &requestId, Json &response)
293{
294 std::lock_guard<std::mutex> lock(m_mutex);
295 Json request;
296 const Status status = ReadRequest(requestId, request);
297 if(!status) return status;
298 response = StatusResponse(request);
299 return {};
300}
301
302XrdHttpTapeApiStore::Status XrdHttpTapeApiStore::ValidateRequestPaths(
303 const Json &request, const Json &paths,
304 std::vector<std::string> &normalized) const
305{
306 std::set<std::string> requestPaths;
307 for(const auto &file : request.at("files"))
308 {
309 requestPaths.insert(file.at("path").get<std::string>());
310 }
311
312 normalized.clear();
313 for(const auto &path : paths)
314 {
315 std::string value;
316 fs::path ignored;
317 Status status = ResolvePath(m_diskRoot, path.get<std::string>(),
318 value, ignored);
319 if(!status) return status;
320 if(requestPaths.count(value) == 0)
321 {
322 return Error(400, "file does not belong to the stage request");
323 }
324 normalized.push_back(std::move(value));
325 }
326 return {};
327}
328
330 const std::string &requestId, const Json &paths)
331{
332 std::lock_guard<std::mutex> lock(m_mutex);
333 Json request;
334 Status status = ReadRequest(requestId, request);
335 if(!status) return status;
336 std::vector<std::string> normalized;
337 return ValidateRequestPaths(request, paths, normalized);
338}
339
341 const std::string &requestId)
342{
343 std::lock_guard<std::mutex> lock(m_mutex);
344 Json request;
345 Status status = ReadRequest(requestId, request);
346 if(!status) return status;
347
348 std::error_code error;
349 if(!fs::remove(m_requestsRoot / (requestId + ".json"), error) || error)
350 {
351 return Error(500, "could not delete the stage request");
352 }
353 return {};
354}
355
357 const std::string &requestId, const Json &paths)
358{
359 std::lock_guard<std::mutex> lock(m_mutex);
360 Json request;
361 Status status = ReadRequest(requestId, request);
362 if(!status) return status;
363
364 std::vector<std::string> normalized;
365 status = ValidateRequestPaths(request, paths, normalized);
366 if(!status) return status;
367
368 for(const auto &path : normalized)
369 {
370 std::string ignored;
371 fs::path diskPath;
372 status = ResolvePath(m_diskRoot, path, ignored, diskPath);
373 if(!status) return status;
374 std::error_code error;
375 fs::remove(diskPath, error);
376 if(error) return Error(500, "could not release the disk replica");
377 }
378 return {};
379}
380
382 const Json &paths, Json &response)
383{
384 std::lock_guard<std::mutex> lock(m_mutex);
385 response = Json::array();
386 for(const auto &path : paths)
387 {
388 Json item;
389 std::string normalized;
390 fs::path archivePath;
391 Status status = ResolvePath(m_archiveRoot, path.get<std::string>(),
392 normalized, archivePath);
393 item["path"] = status ? normalized : path.get<std::string>();
394
395 fs::path diskPath;
396 if(status) status = ResolvePath(m_diskRoot, normalized,
397 normalized, diskPath);
398 bool archived = false;
399 if(status && !IsRegularFile(archivePath, archived))
400 {
401 status = Error(500, "could not inspect the archived file");
402 }
403 bool onDisk = false;
404 if(status && !IsRegularFile(diskPath, onDisk))
405 {
406 status = Error(500, "could not inspect the disk file");
407 }
408
409 if(!status)
410 {
411 item["error"] = status.message;
412 }
413 else if(archived && onDisk)
414 {
415 item["locality"] = "DISK_AND_TAPE";
416 }
417 else if(archived)
418 {
419 item["locality"] = "TAPE";
420 }
421 else if(onDisk)
422 {
423 item["locality"] = "DISK";
424 }
425 else
426 {
427 item["error"] = "file does not exist";
428 }
429 response.push_back(std::move(item));
430 }
431 return {};
432}
Status DeleteStage(const std::string &requestId)
Status GetStage(const std::string &requestId, nlohmann::json &response)
Status CancelStage(const std::string &requestId, const nlohmann::json &paths)
Status CreateStage(const nlohmann::json &files, std::string &requestId)
Status ArchiveInfo(const nlohmann::json &paths, nlohmann::json &response)
XrdHttpTapeApiStore(std::filesystem::path root)
Status Release(const std::string &requestId, const nlohmann::json &paths)
static std::string NormalizePath(const std::string &path)