19#include <system_error>
24using Json = nlohmann::json;
25namespace fs = std::filesystem;
29 return {code, message};
32bool IsWithin(
const fs::path &root,
const fs::path &path)
34 auto rootPart = root.begin();
35 auto pathPart = path.begin();
36 while(rootPart != root.end() && pathPart != path.end())
38 if(*rootPart != *pathPart)
return false;
42 return rootPart == root.end();
45bool IsRegularFile(
const fs::path &path,
bool ®ular)
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)
54 if(error)
return false;
55 regular = fs::is_regular_file(status);
61 : m_root(std::move(root)),
62 m_archiveRoot(m_root /
"archive"),
63 m_diskRoot(m_root /
"disk"),
64 m_requestsRoot(m_root /
"requests")
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");
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";
86std::uint64_t XrdHttpTapeApiStore::Now()
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());
93std::string XrdHttpTapeApiStore::GenerateRequestId()
96 uuid_generate_random(value);
98 uuid_unparse_lower(value, text);
102bool XrdHttpTapeApiStore::IsRequestId(
const std::string &requestId)
104 if(requestId.size() != 36)
return false;
105 for(std::size_t index = 0; index < requestId.size(); ++index)
107 if(index == 8 || index == 13 || index == 18 || index == 23)
109 if(requestId[index] !=
'-')
return false;
111 else if(!std::isxdigit(
static_cast<unsigned char>(requestId[index])))
120 const fs::path &root,
const std::string &path, std::string &normalized,
121 fs::path &resolved)
const
124 if(normalized.empty() || normalized.front() !=
'/')
126 return Error(400,
"file paths must be absolute");
129 const fs::path relative = fs::path(normalized).relative_path();
130 for(
const auto &part : relative)
132 if(part ==
"." || part ==
"..")
134 return Error(400,
"file paths must not contain traversal components");
138 std::error_code error;
139 resolved = fs::weakly_canonical(root / relative, error);
140 if(error || !IsWithin(root, resolved))
142 return Error(400,
"file path is outside the configured storage root");
148 const Json &request)
const
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");
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();
160 std::error_code ignored;
161 fs::remove(temporary, ignored);
162 return Error(500,
"could not write the stage request");
165 std::error_code error;
166 fs::rename(temporary, destination, error);
169 fs::remove(temporary, error);
170 return Error(500,
"could not persist the stage request");
176 const std::string &requestId, Json &request)
const
178 if(!IsRequestId(requestId))
return Error(404,
"unknown stage request");
180 std::ifstream input(m_requestsRoot / (requestId +
".json"),
182 if(!input)
return Error(404,
"unknown stage request");
187 catch(
const std::exception &)
189 return Error(500,
"could not read the stage request");
194Json XrdHttpTapeApiStore::StatusResponse(
const Json &request)
197 {
"id", request.at(
"id")},
198 {
"createdAt", request.at(
"createdAt")},
199 {
"startedAt", request.at(
"startedAt")},
200 {
"files", Json::array()}
202 if(request.contains(
"completedAt"))
204 response[
"completedAt"] = request[
"completedAt"];
207 for(
const auto &stored : request.at(
"files"))
209 Json file = {{
"path", stored.at(
"path")}, {
"state", stored.at(
"state")}};
210 for(
const char *field : {
"startedAt",
"finishedAt",
"error"})
212 if(stored.contains(field)) file[field] = stored[field];
214 response[
"files"].push_back(std::move(file));
220 const Json &files, std::string &requestId)
222 std::lock_guard<std::mutex> lock(m_mutex);
223 requestId = GenerateRequestId();
224 const std::uint64_t createdAt = Now();
227 {
"createdAt", createdAt},
228 {
"startedAt", createdAt},
229 {
"files", Json::array()}
232 for(
const auto &input : files)
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;
244 if(status) status = ResolvePath(m_diskRoot, normalized,
245 normalized, diskPath);
246 bool archived =
false;
247 if(status && !IsRegularFile(archivePath, archived))
249 status =
Error(500,
"could not inspect the archived file");
251 std::error_code error;
252 if(status && archived && fs::file_size(archivePath, error) == 0)
254 status =
Error(400,
"zero-length files cannot be staged from tape");
256 if(error) status =
Error(500,
"could not inspect the archived file");
259 if(status && !IsRegularFile(diskPath, onDisk))
261 status =
Error(500,
"could not inspect the disk file");
263 if(status && archived && !onDisk)
266 fs::create_directories(diskPath.parent_path(), error);
269 fs::copy_file(archivePath, diskPath, fs::copy_options::overwrite_existing,
272 if(error) status =
Error(500,
"could not stage the archived file");
274 file[
"finishedAt"] = Now();
275 if(status && archived)
277 file[
"state"] =
"COMPLETED";
281 file[
"state"] =
"FAILED";
282 file[
"error"] = status ?
"file is not stored on tape" : status.
message;
284 request[
"files"].push_back(std::move(file));
287 request[
"completedAt"] = Now();
288 return WriteRequest(request);
292 const std::string &requestId, Json &response)
294 std::lock_guard<std::mutex> lock(m_mutex);
296 const Status status = ReadRequest(requestId, request);
297 if(!status)
return status;
298 response = StatusResponse(request);
303 const Json &request,
const Json &paths,
304 std::vector<std::string> &normalized)
const
306 std::set<std::string> requestPaths;
307 for(
const auto &file : request.at(
"files"))
309 requestPaths.insert(file.at(
"path").get<std::string>());
313 for(
const auto &path : paths)
317 Status status = ResolvePath(m_diskRoot, path.get<std::string>(),
319 if(!status)
return status;
320 if(requestPaths.count(value) == 0)
322 return Error(400,
"file does not belong to the stage request");
324 normalized.push_back(std::move(value));
330 const std::string &requestId,
const Json &paths)
332 std::lock_guard<std::mutex> lock(m_mutex);
334 Status status = ReadRequest(requestId, request);
335 if(!status)
return status;
336 std::vector<std::string> normalized;
337 return ValidateRequestPaths(request, paths, normalized);
341 const std::string &requestId)
343 std::lock_guard<std::mutex> lock(m_mutex);
345 Status status = ReadRequest(requestId, request);
346 if(!status)
return status;
348 std::error_code error;
349 if(!fs::remove(m_requestsRoot / (requestId +
".json"), error) || error)
351 return Error(500,
"could not delete the stage request");
357 const std::string &requestId,
const Json &paths)
359 std::lock_guard<std::mutex> lock(m_mutex);
361 Status status = ReadRequest(requestId, request);
362 if(!status)
return status;
364 std::vector<std::string> normalized;
365 status = ValidateRequestPaths(request, paths, normalized);
366 if(!status)
return status;
368 for(
const auto &path : normalized)
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");
382 const Json &paths, Json &response)
384 std::lock_guard<std::mutex> lock(m_mutex);
385 response = Json::array();
386 for(
const auto &path : paths)
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>();
396 if(status) status = ResolvePath(m_diskRoot, normalized,
397 normalized, diskPath);
398 bool archived =
false;
399 if(status && !IsRegularFile(archivePath, archived))
401 status =
Error(500,
"could not inspect the archived file");
404 if(status && !IsRegularFile(diskPath, onDisk))
406 status =
Error(500,
"could not inspect the disk file");
411 item[
"error"] = status.
message;
413 else if(archived && onDisk)
415 item[
"locality"] =
"DISK_AND_TAPE";
419 item[
"locality"] =
"TAPE";
423 item[
"locality"] =
"DISK";
427 item[
"error"] =
"file does not exist";
429 response.push_back(std::move(item));
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)