GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/swissknife_ingestsql.cc
Date: 2026-08-30 02:40:36
Exec Total Coverage
Lines: 0 903 0.0%
Branches: 0 594 0.0%

Line Branch Exec Source
1 #include "swissknife_ingestsql.h"
2
3 #include <fcntl.h>
4 #include <grp.h>
5 #include <pwd.h>
6 #include <stdio.h>
7 #include <sys/resource.h>
8 #include <sys/time.h>
9 #include <sys/types.h>
10 #include <unistd.h>
11
12 #include <csignal>
13 #include <cstdlib>
14 #include <fstream>
15 #include <sstream>
16 #include <stack>
17 #include <unordered_map>
18 #include <unordered_set>
19
20 #include "acl.h"
21 #include "catalog_downloader.h"
22 #include "catalog_mgr_rw.h"
23 #include "curl/curl.h"
24 #include "gateway_util.h"
25 #include "shortstring.h"
26 #include "swissknife_lease_curl.h"
27 #include "swissknife_lease_json.h"
28 #include "swissknife_sync.h"
29 #include "upload.h"
30 #include "util/logging.h"
31
32 #define CHECK_SQLITE_ERROR(ret, expected) \
33 do { \
34 const int sqlite_result = (ret); \
35 if (sqlite_result != (expected)) { \
36 LogCvmfs(kLogCvmfs, kLogStderr, "SQLite error: %d", \
37 sqlite_result); \
38 abort(); \
39 } \
40 } while (0)
41
42 #define CUSTOM_ASSERT(check, msg, ...) \
43 do { \
44 if (!(check)) { \
45 LogCvmfs(kLogCvmfs, kLogStderr, msg, ##__VA_ARGS__); \
46 abort(); \
47 } \
48 } while (0)
49
50 #define SHOW_PROGRESS(item, freq, curr, total) \
51 do { \
52 if ((curr) % freq == 0 || (curr) == total) { \
53 LogCvmfs(kLogCvmfs, kLogStdout, "Processed %d/%d %s", (curr), total, \
54 item); \
55 } \
56 } while (0)
57
58
59 static const unsigned kExternalChunkSize = 24 * 1024 * 1024;
60 static const unsigned kInternalChunkSize = 6 * 1024 * 1024;
61 static const unsigned kDefaultLeaseBusyRetryInterval = 10;
62 static const unsigned kLeaseRefreshInterval = 90; // seconds
63
64
65 static bool g_lease_acquired = false;
66 static string g_gateway_url;
67 static string g_gateway_key_id;
68 static string g_gateway_secret;
69 static string g_session_token;
70 static string g_session_token_file;
71 static string g_s3_file;
72 static time_t g_last_lease_refresh = 0;
73 static bool g_stop_refresh = false;
74 static string g_wait_for_update;
75 static int64_t g_priority = 0;
76 static bool g_add_missing_catalogs = false;
77 static string get_lease_from_paths(vector<string> paths);
78 static vector<string> get_all_dirs_from_sqlite(vector<string> &sqlite_db_vec,
79 bool include_additions,
80 bool include_deletions);
81 static string get_parent(const string &path);
82 static string get_basename(const string &path);
83
84 static XattrList marshal_xattrs(const char *acl);
85 static string sanitise_name(const char *name_cstr, bool allow_leading_slash);
86 static void on_signal(int sig);
87 static string acquire_lease(const string &key_id, const string &secret,
88 const string &lease_path,
89 const string &repo_service_url,
90 bool force_cancel_lease, uint64_t *current_revision,
91 string &current_root_hash,
92 unsigned int refresh_interval);
93 static void cancel_lease();
94 static void refresh_lease();
95 static vector<string> get_file_list(string &path);
96 static int check_hash(const char *hash);
97 static void recursively_delete_directory(
98 PathString &path, catalog::WritableCatalogManager &catalog_manager);
99 static void create_empty_database(string &filename);
100 static void relax_db_locking(sqlite3 *db);
101 static bool check_prefix(const std::string &path, const std::string &prefix);
102
103 static bool isDatabaseMarkedComplete(const char *dbfile);
104 static void setDatabaseMarkedComplete(const char *dbfile);
105
106 static void invalidate_manifest(std::string proxy_list, std::string url);
107 static void wait_for_update(std::string path, long revision);
108
109 extern "C" void *lease_refresh_thread(void *payload);
110
111 extern long g_final_revision;
112
113
114 static string sanitise_name(const char *name_cstr,
115 bool allow_leading_slash = false) {
116 int reason = 0;
117 const char *c = name_cstr;
118 while (*c == '/') {
119 c++;
120 } // strip any leading slashes
121 string const name = string(c);
122 bool ok = true;
123
124 if (!allow_leading_slash && HasPrefix(name, "/", true)) {
125 reason = 1;
126 ok = false;
127 }
128 if (HasSuffix(name, "/", true)) {
129 if (!(allow_leading_slash
130 && name.size() == 1)) { // account for the case where name=="/"
131 reason = 2;
132 ok = false;
133 }
134 }
135 if (name.find("//") != string::npos) {
136 reason = 3;
137 ok = false;
138 }
139 if (HasPrefix(name, "./", true) || HasPrefix(name, "../", true)) {
140 reason = 4;
141 ok = false;
142 }
143 if (HasSuffix(name, "/.", true) || HasSuffix(name, "/..", true)) {
144 reason = 5;
145 ok = false;
146 }
147 if (name.find("/./") != string::npos || name.find("/../") != string::npos) {
148 reason = 6;
149 ok = false;
150 }
151 if (name == "") {
152 reason = 7;
153 ok = false;
154 }
155 CUSTOM_ASSERT(ok, "Name [%s] is invalid (reason %d)", name.c_str(), reason);
156 return string(name);
157 }
158
159 static string get_parent(const string &path) {
160 size_t const found = path.find_last_of('/');
161 if (found == string::npos) {
162 return string("");
163 }
164 return path.substr(0, found);
165 }
166
167 static string get_basename(const string &path) {
168 const size_t found = path.find_last_of('/');
169 if (found == string::npos) {
170 return path;
171 }
172 return path.substr(found + 1);
173 }
174
175 // this is copied from MakeRelativePath
176 static string MakeCatalogPath(const std::string &relative_path) {
177 return (relative_path == "") ? "" : "/" + relative_path;
178 }
179
180 static string acquire_lease(const string &key_id, const string &secret,
181 const string &lease_path,
182 const string &repo_service_url,
183 bool force_cancel_lease, uint64_t *current_revision,
184 string &current_root_hash,
185 unsigned int refresh_interval) {
186 const CURLcode ret = curl_global_init(CURL_GLOBAL_ALL);
187 CUSTOM_ASSERT(ret == CURLE_OK, "failed to init curl");
188
189 string gateway_metadata_str;
190 char *gateway_metadata = getenv("CVMFS_GATEWAY_METADATA");
191 if (gateway_metadata != NULL)
192 gateway_metadata_str = gateway_metadata;
193
194 while (true) {
195 CurlBuffer buffer;
196 if (MakeAcquireRequest(key_id, secret, lease_path, repo_service_url,
197 &buffer, gateway_metadata_str)) {
198 string session_token;
199
200 const LeaseReply rep = ParseAcquireReplyWithRevision(
201 buffer, &session_token, current_revision, current_root_hash);
202 switch (rep) {
203 case kLeaseReplySuccess:
204 g_lease_acquired = true;
205 g_last_lease_refresh = time(NULL);
206 return session_token;
207 break;
208 case kLeaseReplyBusy:
209 if (force_cancel_lease) {
210 LogCvmfs(kLogCvmfs, kLogStderr,
211 "Lease busy, forcing cancellation (TODO");
212 }
213 LogCvmfs(kLogCvmfs, kLogStderr, "Lease busy, retrying in %d sec",
214 refresh_interval);
215 sleep(refresh_interval);
216 break;
217 default:
218 LogCvmfs(kLogCvmfs, kLogStderr,
219 "Error acquiring lease: %s. Retrying in %d sec",
220 buffer.data.c_str(), refresh_interval);
221 sleep(refresh_interval);
222 }
223 } else {
224 LogCvmfs(kLogCvmfs, kLogStderr,
225 "Error making lease acquisition request. Retrying in %d sec",
226 refresh_interval);
227 sleep(refresh_interval);
228 }
229 }
230 assert(false);
231 return "";
232 }
233
234 static uint64_t make_commit_on_gateway(const std::string &old_root_hash,
235 const std::string &new_root_hash,
236 int64_t priority) {
237 CurlBuffer buffer;
238 char priorityStr[100];
239 (void)sprintf(priorityStr, "%" PRId64,
240 priority); // skipping return value check; no way such large
241 // buffer will overflow
242 buffer.data = "";
243
244 const std::string payload = "{\n\"old_root_hash\": \"" + old_root_hash
245 + "\",\n\"new_root_hash\": \"" + new_root_hash
246 + "\",\n\"priority\": " + priorityStr + "}";
247
248 return MakeEndRequest("POST", g_gateway_key_id, g_gateway_secret,
249 g_session_token, g_gateway_url, payload, &buffer,
250 true /*expect_final_revision*/);
251 }
252
253 static void refresh_lease() {
254 CurlBuffer buffer;
255 buffer.data = "";
256 if ((time(NULL) - g_last_lease_refresh) < kLeaseRefreshInterval) {
257 return;
258 }
259
260 if (MakeEndRequest("PATCH", g_gateway_key_id, g_gateway_secret,
261 g_session_token, g_gateway_url, "", &buffer,
262 false /*expect_final_revision*/)) {
263 const int ret = ParseDropReply(buffer);
264 if (kLeaseReplySuccess == ret) {
265 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Lease refreshed");
266 g_last_lease_refresh = time(NULL);
267 } else {
268 LogCvmfs(kLogCvmfs, kLogStderr, "Lease refresh failed: %d", ret);
269 }
270 } else {
271 LogCvmfs(kLogCvmfs, kLogStderr, "Lease refresh request failed");
272 if (buffer.data == "Method Not Allowed\n") {
273 g_last_lease_refresh = time(NULL);
274 LogCvmfs(kLogCvmfs, kLogStderr,
275 "This gateway does not support lease refresh");
276 }
277 }
278 }
279
280
281 static void cancel_lease() {
282 CurlBuffer buffer;
283 if (MakeEndRequest("DELETE", g_gateway_key_id, g_gateway_secret,
284 g_session_token, g_gateway_url, "", &buffer,
285 false /*expect_final_revision*/)) {
286 const int ret = ParseDropReply(buffer);
287 if (kLeaseReplySuccess == ret) {
288 LogCvmfs(kLogCvmfs, kLogStdout, "Lease cancelled");
289 } else {
290 LogCvmfs(kLogCvmfs, kLogStderr, "Lease cancellation failed: %d", ret);
291 }
292 } else {
293 LogCvmfs(kLogCvmfs, kLogStderr, "Lease cancellation request failed");
294 }
295 g_stop_refresh = true;
296 }
297
298 static void on_signal(int sig) {
299 (void)signal(sig, SIG_DFL);
300 if (g_lease_acquired) {
301 LogCvmfs(kLogCvmfs, kLogStdout, "Cancelling lease");
302 cancel_lease();
303 unlink(g_session_token_file.c_str());
304 }
305 if (sig == SIGINT || sig == SIGTERM)
306 exit(1);
307 }
308
309 static vector<string> get_all_dirs_from_sqlite(vector<string> &sqlite_db_vec,
310 bool include_additions,
311 bool include_deletions) {
312 int ret;
313 vector<string> paths;
314
315 for (vector<string>::iterator it = sqlite_db_vec.begin();
316 it != sqlite_db_vec.end();
317 it++) {
318 sqlite3 *db;
319 ret = sqlite3_open_v2((*it).c_str(), &db, SQLITE_OPEN_READONLY, NULL);
320 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
321 relax_db_locking(db);
322
323 vector<string> tables;
324 if (include_additions) {
325 tables.push_back("dirs");
326 tables.push_back("links");
327 tables.push_back("files");
328 }
329 if (include_deletions) {
330 tables.push_back("deletions");
331 }
332
333 // get all the paths from the DB
334 for (vector<string>::iterator it = tables.begin(); it != tables.end();
335 it++) {
336 sqlite3_stmt *stmt;
337 const string query = "SELECT name FROM " + *it;
338 ret = sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, NULL);
339 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
340 while (sqlite3_step(stmt) == SQLITE_ROW) {
341 const char *name = reinterpret_cast<const char *>(
342 sqlite3_column_text(stmt, 0));
343 const string names = sanitise_name(name);
344 if (*it == "dirs") {
345 paths.push_back(names);
346 } else {
347 paths.push_back(get_parent(names));
348 }
349 }
350 ret = sqlite3_finalize(stmt);
351 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
352 }
353 ret = sqlite3_close_v2(db);
354 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
355 }
356 return paths;
357 }
358
359 static int get_db_schema_revision(sqlite3 *db,
360 const std::string &db_name = "") {
361 sqlite3_stmt *stmt;
362 std::ostringstream stmt_str;
363 stmt_str << "SELECT value FROM " << db_name
364 << "properties WHERE key = 'schema_revision'";
365 int ret = sqlite3_prepare_v2(db, stmt_str.str().c_str(), -1, &stmt, NULL);
366 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
367
368 ret = sqlite3_step(stmt);
369 // if table exists, we require that it must have a schema_revision row
370 CHECK_SQLITE_ERROR(ret, SQLITE_ROW);
371 const std::string schema_revision_str(
372 reinterpret_cast<const char *>(sqlite3_column_text(stmt, 0)));
373 CHECK_SQLITE_ERROR(sqlite3_finalize(stmt), SQLITE_OK);
374 return std::stoi(schema_revision_str);
375 }
376
377 static int get_row_count(sqlite3 *db, const std::string &table_name) {
378 sqlite3_stmt *stmt;
379 std::ostringstream stmt_str;
380 stmt_str << "SELECT COUNT(*) FROM " << table_name;
381 int ret = sqlite3_prepare_v2(db, stmt_str.str().c_str(), -1, &stmt, NULL);
382 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
383
384 ret = sqlite3_step(stmt);
385 CHECK_SQLITE_ERROR(ret, SQLITE_ROW);
386 const std::string count_str(
387 reinterpret_cast<const char *>(sqlite3_column_text(stmt, 0)));
388 CHECK_SQLITE_ERROR(sqlite3_finalize(stmt), SQLITE_OK);
389 return std::stoi(count_str);
390 }
391
392 static int calculate_print_frequency(int total) {
393 int base = 1000;
394 while (base * 50 < total)
395 base *= 10;
396 return base;
397 }
398
399 // compute a common path among for paths for use as the lease path
400 static string get_lease_from_paths(vector<string> paths) {
401 CUSTOM_ASSERT(!paths.empty(), "no paths are provided");
402
403 // we'd have to ensure path is relative
404 // (probably best to check this elsewhere as it is not just a requirement for
405 // this function)
406 auto lease = PathString(paths.at(0));
407 for (auto it = paths.begin() + 1; it != paths.end(); ++it) {
408 auto path = PathString(*it);
409 // shrink the lease path until it is a parent of "path"
410 while (!IsSubPath(lease, path)) {
411 auto i = lease.GetLength() - 1;
412 for (; i >= 0; --i) {
413 if (lease.GetChars()[i] == '/' || i == 0) {
414 lease.Truncate(i);
415 break;
416 }
417 }
418 }
419 if (lease.IsEmpty())
420 break; // early stop if lease is already at the root
421 }
422
423 auto prefix = "/" + lease.ToString();
424
425 LogCvmfs(kLogCvmfs, kLogStdout, "Longest prefix is %s", prefix.c_str());
426 return prefix;
427 }
428
429 static XattrList marshal_xattrs(const char *acl_string) {
430 XattrList aclobj;
431
432 if (acl_string == NULL || acl_string[0] == '\0') {
433 return aclobj;
434 }
435
436 bool equiv_mode;
437 size_t binary_size;
438 char *binary_acl;
439 const int ret = acl_from_text_to_xattr_value(string(acl_string), binary_acl,
440 binary_size, equiv_mode);
441 if (ret) {
442 LogCvmfs(kLogCvmfs, kLogStderr,
443 "failure of acl_from_text_to_xattr_value(%s)", acl_string);
444 abort();
445 }
446 if (!equiv_mode) {
447 CUSTOM_ASSERT(
448 aclobj.Set("system.posix_acl_access", string(binary_acl, binary_size)),
449 "failed to set system.posix_acl_access (ACL size %ld)", binary_size);
450 free(binary_acl);
451 }
452
453 return aclobj;
454 }
455
456 std::unordered_map<string, string> load_config(const string &config_file) {
457 std::unordered_map<string, string> config_map;
458 ifstream input(config_file);
459 if (!input) {
460 LogCvmfs(kLogCvmfs, kLogStderr, "could not open config file %s",
461 config_file.c_str());
462 return config_map;
463 }
464 vector<string> lines;
465 for (string line; getline(input, line);) {
466 lines.push_back(line);
467 }
468
469 for (auto it = lines.begin(); it != lines.end(); it++) {
470 const string l = *it;
471 const size_t p = l.find('=', 0);
472 if (p != string::npos) {
473 const string key = l.substr(0, p);
474 string val = l.substr(p + 1);
475 // trim any double quotes
476 if (val.front() == '"') {
477 val = val.substr(1, val.length() - 2);
478 }
479 config_map[key] = val;
480 }
481 }
482
483 return config_map;
484 }
485
486 string retrieve_config(std::unordered_map<string, string> &config_map,
487 const string &key) {
488 auto kv = config_map.find(key);
489 CUSTOM_ASSERT(kv != config_map.end(), "Parameter %s not found in config",
490 key.c_str());
491 return kv->second;
492 }
493
494 static vector<string> get_file_list(string &path) {
495 vector<string> paths;
496 const char *cpath = path.c_str();
497 struct stat st;
498 const int ret = stat(cpath, &st);
499 CUSTOM_ASSERT(ret == 0, "failed to stat file %s", cpath);
500
501 if (S_ISDIR(st.st_mode)) {
502 DIR *d;
503 struct dirent *dir;
504 d = opendir(cpath);
505 if (d) {
506 while ((dir = readdir(d)) != NULL) {
507 const char *t = strrchr(dir->d_name, '.');
508 if (t && !strcmp(t, ".db")) {
509 paths.push_back(path + "/" + dir->d_name);
510 }
511 }
512 closedir(d);
513 }
514 } else {
515 paths.push_back(path);
516 }
517 return paths;
518 }
519
520 extern bool g_log_with_time;
521
522 int swissknife::IngestSQL::Main(const swissknife::ArgumentList &args) {
523 // the catalog code uses assert() liberally.
524 // install ABRT signal handler to catch an abort and cancel lease
525 if (signal(SIGABRT, &on_signal) == SIG_ERR
526 || signal(SIGINT, &on_signal) == SIG_ERR
527 || signal(SIGTERM, &on_signal) == SIG_ERR) {
528 LogCvmfs(kLogCvmfs, kLogStdout, "Setting signal handlers failed");
529 exit(1);
530 }
531
532 const bool enable_corefiles = (args.find('c') != args.end());
533 if (!enable_corefiles) {
534 struct rlimit rlim;
535 rlim.rlim_cur = rlim.rlim_max = 0;
536 setrlimit(RLIMIT_CORE, &rlim);
537 }
538
539
540 if (args.find('n') != args.end()) {
541 create_empty_database(*args.find('n')->second);
542 exit(0);
543 }
544
545 if (args.find('B') != args.end()) {
546 g_wait_for_update = *args.find('B')->second;
547 }
548
549 if (args.find('P') != args.end()) {
550 const char *arg = (*args.find('P')->second).c_str();
551 char *at_null_terminator_if_number;
552 g_priority = strtoll(arg, &at_null_terminator_if_number, 10);
553 if (*at_null_terminator_if_number != '\0') {
554 LogCvmfs(kLogCvmfs, kLogStderr,
555 "Priority parameter value '%s' parsing failed", arg);
556 return 1;
557 }
558 } else {
559 g_priority = -time(NULL);
560 }
561
562
563 unsigned int lease_busy_retry_interval = kDefaultLeaseBusyRetryInterval;
564 if (args.find('r') != args.end()) {
565 lease_busy_retry_interval = atoi((*args.find('r')->second).c_str());
566 }
567
568 string dir_temp = "";
569 const char *env_tmpdir;
570 if (args.find('t') != args.end()) {
571 dir_temp = MakeCanonicalPath(*args.find('t')->second);
572 } else if ((env_tmpdir = getenv("TMPDIR"))) {
573 dir_temp = MakeCanonicalPath(env_tmpdir);
574 } else {
575 LogCvmfs(kLogCvmfs, kLogStderr, "-t or TMPDIR required");
576 return 1;
577 }
578
579 string kConfigDir("/etc/cvmfs/gateway-client/");
580 if (args.find('C') != args.end()) {
581 kConfigDir = MakeCanonicalPath(*args.find('C')->second);
582 kConfigDir += "/";
583 LogCvmfs(kLogCvmfs, kLogStdout, "Overriding configuration dir prefix to %s",
584 kConfigDir.c_str());
585 }
586
587 // mandatory arguments
588 string const repo_name = *args.find('N')->second;
589 string sqlite_db_path = *args.find('D')->second;
590
591 vector<string> sqlite_db_vec = get_file_list(sqlite_db_path);
592
593 // optional arguments
594 bool const allow_deletions = (args.find('d') != args.end());
595 bool const force_cancel_lease = (args.find('x') != args.end());
596 bool const allow_additions = !allow_deletions
597 || (args.find('a') != args.end());
598 g_add_missing_catalogs = (args.find('z') != args.end());
599 bool const check_completed_graft_property = (args.find('Z') != args.end());
600 if (args.find('v') != args.end()) {
601 SetLogVerbosity(kLogVerbose);
602 }
603
604 if (check_completed_graft_property) {
605 if (sqlite_db_vec.size() != 1) {
606 LogCvmfs(kLogCvmfs, kLogStderr, "-Z requires a single DB file");
607 exit(1);
608 }
609 if (isDatabaseMarkedComplete(sqlite_db_vec[0].c_str())) {
610 LogCvmfs(kLogCvmfs, kLogStderr,
611 "DB file is already marked as completed_graft");
612 exit(0);
613 } else {
614 LogCvmfs(kLogCvmfs, kLogStderr,
615 "DB file is not marked as completed_graft");
616 }
617 }
618
619 string const config_file = kConfigDir + repo_name + "/config";
620 string stratum0;
621 string proxy;
622
623 string additional_prefix = "";
624 bool has_additional_prefix = false;
625 if (args.find('p') != args.end()) {
626 additional_prefix = *args.find('p')->second;
627 additional_prefix = sanitise_name(additional_prefix.c_str(), true);
628 if (additional_prefix.back() != '/') {
629 additional_prefix += "/";
630 }
631 has_additional_prefix = true;
632 LogCvmfs(kLogCvmfs, kLogStdout,
633 "Adding additional prefix %s to lease and all paths",
634 additional_prefix.c_str());
635 // now we are confident that any additional prefix has no leading / and does
636 // have a tailing /
637 }
638 auto config_map = load_config(config_file);
639
640 if (args.find('g') != args.end()) {
641 g_gateway_url = *args.find('g')->second;
642 } else {
643 g_gateway_url = retrieve_config(config_map, "CVMFS_GATEWAY");
644 }
645 if (args.find('w') != args.end()) {
646 stratum0 = *args.find('w')->second;
647 } else {
648 stratum0 = retrieve_config(config_map, "CVMFS_STRATUM0");
649 }
650
651 if (args.find('@') != args.end()) {
652 proxy = *args.find('@')->second;
653 } else {
654 proxy = retrieve_config(config_map, "CVMFS_HTTP_PROXY");
655 }
656
657 string lease_path = "";
658 // bool lease_autodetected = false;
659 if (args.find('l') != args.end()) {
660 lease_path = *args.find('l')->second;
661 } else {
662 // lease path wasn't specified, so try to autodetect it
663 vector<string> const paths = get_all_dirs_from_sqlite(
664 sqlite_db_vec, allow_additions, allow_deletions);
665 if (paths.size() == 0) {
666 LogCvmfs(kLogCvmfs, kLogStdout, "Database is empty, nothing to do");
667 return 0; // treat it as a success
668 }
669 lease_path = get_lease_from_paths(paths);
670 // lease_autodetected = true;
671 }
672
673 if (has_additional_prefix) {
674 if (lease_path == "/") {
675 lease_path = "/" + additional_prefix;
676 } else {
677 if (lease_path.substr(0, 1) == "/") {
678 lease_path = "/" + additional_prefix
679 + lease_path.substr(1, lease_path.size() - 1);
680 } else {
681 lease_path = "/" + additional_prefix
682 + lease_path; // prefix is certain to have a trailing /
683 }
684 }
685 }
686 if (lease_path.substr(0, 1) != "/") {
687 lease_path = "/" + lease_path;
688 }
689 LogCvmfs(kLogCvmfs, kLogStdout, "Lease path is %s", lease_path.c_str());
690
691
692 string public_keys = kConfigDir + repo_name + "/pubkey";
693 string key_file = kConfigDir + repo_name + "/gatewaykey";
694 string s3_file = kConfigDir + repo_name + "/s3.conf";
695
696 if (args.find('k') != args.end()) {
697 public_keys = *args.find('k')->second;
698 }
699 if (args.find('s') != args.end()) {
700 key_file = *args.find('s')->second;
701 }
702 if (args.find('3') != args.end()) {
703 s3_file = *args.find('3')->second;
704 }
705
706 CUSTOM_ASSERT(access(public_keys.c_str(), R_OK) == 0, "%s is not readable",
707 public_keys.c_str());
708 CUSTOM_ASSERT(access(key_file.c_str(), R_OK) == 0, "%s is not readable",
709 key_file.c_str());
710
711 // string spooler_definition_string = string("gw,,") + g_gateway_url;
712 // create a spooler that will upload to S3
713 string const spooler_definition_string = string("S3,") + dir_temp + ","
714 + repo_name + "@" + s3_file;
715
716 // load gateway lease
717 if (!gateway::ReadKeys(key_file, &g_gateway_key_id, &g_gateway_secret)) {
718 LogCvmfs(kLogCvmfs, kLogStderr, "gateway::ReadKeys failed");
719 return 1;
720 }
721
722 uint64_t current_revision = 0;
723 std::string current_root_hash = "";
724
725 // acquire lease and save token to a file in the tmpdir
726 LogCvmfs(kLogCvmfs, kLogStdout, "Acquiring gateway lease on %s",
727 lease_path.c_str());
728 g_session_token = acquire_lease(g_gateway_key_id, g_gateway_secret,
729 repo_name + lease_path, g_gateway_url,
730 force_cancel_lease, &current_revision,
731 current_root_hash, lease_busy_retry_interval);
732
733
734 char *_tmpfile = strdup((dir_temp + "/gateway_session_token_XXXXXX").c_str());
735 int const temp_fd = mkstemp(_tmpfile);
736 g_session_token_file = string(_tmpfile);
737 free(_tmpfile);
738
739 FILE *fout = fdopen(temp_fd, "wb");
740 CUSTOM_ASSERT(fout != NULL,
741 "failed to open session token file %s for writing",
742 g_session_token_file.c_str());
743 fputs(g_session_token.c_str(), fout);
744 fclose(fout);
745
746 // now start the lease refresh thread
747 pthread_t lease_thread;
748 if (0 != pthread_create(&lease_thread, NULL, lease_refresh_thread, NULL)) {
749 LogCvmfs(kLogCvmfs, kLogStderr, "Unable to start lease refresh thread");
750 cancel_lease();
751 return 1;
752 }
753
754 // now initialise the various bits we need
755
756 upload::SpoolerDefinition spooler_definition(
757 spooler_definition_string, shash::kSha1, zlib::kZlibDefault, false, true,
758 SyncParameters::kDefaultMinFileChunkSize,
759 SyncParameters::kDefaultAvgFileChunkSize,
760 SyncParameters::kDefaultMaxFileChunkSize, g_session_token_file, key_file);
761
762 if (args.find('q') != args.end()) {
763 spooler_definition.number_of_concurrent_uploads = String2Uint64(
764 *args.find('q')->second);
765 }
766
767 upload::SpoolerDefinition const spooler_definition_catalogs(
768 spooler_definition.Dup2DefaultCompression());
769
770 std::unique_ptr<upload::Spooler> const spooler_catalogs(
771 upload::Spooler::Construct(spooler_definition_catalogs, nullptr));
772
773 if (spooler_catalogs.get() == nullptr) {
774 LogCvmfs(kLogCvmfs, kLogStderr, "spooler_catalogs invalid");
775 cancel_lease();
776 return 1;
777 }
778 if (!InitDownloadManager(true, proxy, kCatalogDownloadMultiplier)) {
779 LogCvmfs(kLogCvmfs, kLogStderr, "download manager init failed");
780 cancel_lease();
781 return 1;
782 }
783 if (!InitSignatureManager(public_keys, "")) {
784 LogCvmfs(kLogCvmfs, kLogStderr, "signature manager init failed");
785 cancel_lease();
786 return 1;
787 }
788
789 std::unique_ptr<manifest::Manifest> manifest;
790
791 manifest.reset(FetchRemoteManifest(stratum0, repo_name, shash::Any()));
792
793 if (manifest.get() == nullptr) {
794 LogCvmfs(kLogCvmfs, kLogStderr, "manifest invalid");
795 cancel_lease();
796 return 1;
797 }
798
799 if (current_revision > 0) {
800 if (current_revision == manifest->revision()) {
801 if (current_root_hash != manifest->catalog_hash().ToString()) {
802 LogCvmfs(kLogCvmfs, kLogStderr,
803 "Mismatch between cvmfspublished and gateway hash for "
804 "revision %lu (%s!=%s)",
805 current_revision, current_root_hash.c_str(),
806 manifest->catalog_hash().ToString().c_str());
807 cancel_lease();
808 return 1;
809 } else {
810 LogCvmfs(kLogCvmfs, kLogStdout,
811 "Gateway and .cvmfspublished agree on repo version %lu",
812 current_revision);
813 }
814 }
815 if (current_revision > manifest->revision()) {
816 LogCvmfs(kLogCvmfs, kLogStdout,
817 "Gateway has supplied a newer revision than the current "
818 ".cvmfspublished %lu > %lu",
819 current_revision, manifest->revision());
820 manifest->set_revision(current_revision);
821 manifest->set_catalog_hash(shash::MkFromHexPtr(
822 shash::HexPtr(current_root_hash), shash::kSuffixCatalog));
823 } else if (current_revision < manifest->revision()) {
824 LogCvmfs(kLogCvmfs, kLogStdout,
825 "Gateway has supplied an older revision than the current "
826 ".cvmfspublished %lu < %lu",
827 current_revision, manifest->revision());
828 }
829 } else {
830 LogCvmfs(kLogCvmfs, kLogStdout,
831 "Gateway has not supplied a revision. Using .cvmfspublished");
832 }
833
834
835 // get hash of current root catalog, remove terminal "C", encode it
836 string const old_root_hash = manifest->catalog_hash().ToString(true);
837 string const hash = old_root_hash.substr(0, old_root_hash.length() - 1);
838 shash::Any const base_hash = shash::MkFromHexPtr(shash::HexPtr(hash),
839 shash::kSuffixCatalog);
840 LogCvmfs(kLogCvmfs, kLogStdout, "old_root_hash: %s", old_root_hash.c_str());
841
842 bool const is_balanced = false;
843
844 catalog::WritableCatalogManager catalog_manager(
845 base_hash, stratum0, dir_temp, spooler_catalogs.get(), download_manager(),
846 false, SyncParameters::kDefaultNestedKcatalogLimit,
847 SyncParameters::kDefaultRootKcatalogLimit,
848 SyncParameters::kDefaultFileMbyteLimit, statistics(), is_balanced,
849 SyncParameters::kDefaultMaxWeight, SyncParameters::kDefaultMinWeight,
850 dir_temp /* dir_cache */);
851
852 catalog_manager.Init();
853
854
855 // now graft the contents of the DB
856 vector<sqlite3 *> open_dbs;
857 for (auto &&db_file : sqlite_db_vec) {
858 sqlite3 *db;
859 CHECK_SQLITE_ERROR(
860 sqlite3_open_v2(db_file.c_str(), &db, SQLITE_OPEN_READONLY, NULL),
861 SQLITE_OK);
862 relax_db_locking(db);
863 open_dbs.push_back(db);
864 }
865 process_sqlite(open_dbs, catalog_manager, allow_additions, allow_deletions,
866 lease_path.substr(1), additional_prefix);
867 for (auto &&db : open_dbs) {
868 CHECK_SQLITE_ERROR(sqlite3_close_v2(db), SQLITE_OK);
869 }
870
871 // commit changes
872 LogCvmfs(kLogCvmfs, kLogStdout, "Committing changes...");
873 if (!catalog_manager.Commit(false, false, manifest.get())) {
874 LogCvmfs(kLogCvmfs, kLogStderr, "something went wrong during sync");
875 cancel_lease();
876 return 1;
877 }
878
879 // finalize the spooler
880 LogCvmfs(kLogCvmfs, kLogStdout, "Waiting for all uploads to finish...");
881 spooler_catalogs->WaitForUpload();
882
883 LogCvmfs(kLogCvmfs, kLogStdout, "Exporting repository manifest");
884
885 // We call FinalizeSession(true) this time, to also trigger the commit
886 // operation on the gateway machine (if the upstream is of type "gw").
887
888 // Get the path of the new root catalog
889 const string new_root_hash = manifest->catalog_hash().ToString(true);
890
891 // if (!spooler_catalogs->FinalizeSession(true, old_root_hash, new_root_hash,
892 // RepositoryTag())) {
893 // LogCvmfs(kLogCvmfs, kLogStderr, "Failed to commit the transaction");
894 // // lease is only released on success
895 // cancel_lease();
896 // return 1;
897 // }
898
899 LogCvmfs(kLogCvmfs, kLogStdout, "Committing with priority %" PRId64,
900 g_priority);
901
902 bool const ok = make_commit_on_gateway(old_root_hash, new_root_hash,
903 g_priority);
904 if (!ok) {
905 LogCvmfs(kLogCvmfs, kLogStderr,
906 "something went wrong during commit on gateway");
907 cancel_lease();
908 exit(1);
909 }
910
911
912 unlink(g_session_token_file.c_str());
913
914 g_stop_refresh = true;
915
916 if (g_wait_for_update != "") {
917 invalidate_manifest(proxy, stratum0 + "/.cvmfspublished");
918 wait_for_update(g_wait_for_update, g_final_revision);
919 }
920
921 if (check_completed_graft_property) {
922 setDatabaseMarkedComplete(sqlite_db_vec[0].c_str());
923 }
924
925
926 return 0;
927 }
928
929 size_t writeFunction(void *ptr, size_t size, size_t nmemb, std::string *data) {
930 return size * nmemb;
931 }
932
933 void replaceAllSubstrings(std::string &str, const std::string &from,
934 const std::string &to) {
935 if (from.empty()) {
936 return; // Avoid infinite loop if 'from' is an empty string.
937 }
938 size_t startPos = 0;
939 while ((startPos = str.find(from, startPos)) != std::string::npos) {
940 str.replace(startPos, from.length(), to);
941 startPos += to.length(); // Advance startPos to avoid replacing the
942 // substring just inserted.
943 }
944 }
945
946
947 void swissknife::IngestSQL::process_sqlite(
948 const std::vector<sqlite3 *> &dbs,
949 catalog::WritableCatalogManager &catalog_manager, bool allow_additions,
950 bool allow_deletions, const std::string &lease_path,
951 const std::string &additional_prefix) {
952 std::map<std::string, Directory> all_dirs;
953 std::map<std::string, std::vector<File> > all_files;
954 std::map<std::string, std::vector<Symlink> > all_symlinks;
955
956 for (auto &&db : dbs) {
957 load_dirs(db, lease_path, additional_prefix, all_dirs);
958 }
959
960 // put in a nested scope so we can free up memory of `dir_names`
961 {
962 LogCvmfs(kLogCvmfs, kLogStdout,
963 "Precaching existing directories (starting from %s)",
964 lease_path.c_str());
965 std::unordered_set<std::string> dir_names;
966 std::transform(all_dirs.begin(), all_dirs.end(),
967 std::inserter(dir_names, dir_names.end()),
968 [](const std::pair<std::string, Directory> &pair) {
969 return MakeCatalogPath(pair.first);
970 });
971 catalog_manager.LoadCatalogs(MakeCatalogPath(lease_path), dir_names);
972 }
973
974 for (auto &&db : dbs) {
975 load_files(db, lease_path, additional_prefix, all_files);
976 load_symlinks(db, lease_path, additional_prefix, all_symlinks);
977 }
978
979 // perform all deletions first
980 if (allow_deletions) {
981 LogCvmfs(kLogCvmfs, kLogStdout, "Processing deletions...");
982 for (auto &&db : dbs) {
983 CHECK_SQLITE_ERROR(
984 do_deletions(db, catalog_manager, lease_path, additional_prefix),
985 SQLITE_OK);
986 }
987 }
988
989 if (allow_additions) {
990 LogCvmfs(kLogCvmfs, kLogStdout, "Processing additions...");
991 // first ensure all directories are present and create missing ones
992 do_additions(all_dirs, all_files, all_symlinks, lease_path,
993 catalog_manager);
994 }
995 }
996
997 void add_dir_to_tree(
998 std::string path,
999 std::unordered_map<std::string, std::set<std::string> > &tree,
1000 const std::string &lease_path) {
1001 tree[path];
1002 std::string parent_path = get_parent(path);
1003 // recursively create any missing parents in the tree
1004 // avoid creating a loop when we insert the root path
1005 while (path != parent_path && path != lease_path
1006 && !tree[parent_path].count(path)) {
1007 tree[parent_path].insert(path);
1008 path = parent_path;
1009 parent_path = get_parent(path);
1010 }
1011 }
1012
1013 int swissknife::IngestSQL::do_additions(
1014 const DirMap &all_dirs, const FileMap &all_files,
1015 const SymlinkMap &all_symlinks, const std::string &lease_path,
1016 catalog::WritableCatalogManager &catalog_manager) {
1017 // STEP 1:
1018 // - collect all the dirs/symlinks/files we need to process from the DB
1019 // - build a tree of paths for DFS traversal
1020 // - note the tree will contain all parent dirs of symlinks/files even if
1021 // those are not
1022 // explicitly added to the dirs table
1023 std::unordered_map<std::string, std::set<std::string> > tree;
1024 for (auto &&p : all_dirs) {
1025 add_dir_to_tree(p.first, tree, lease_path);
1026 }
1027 for (auto &&p : all_files) {
1028 add_dir_to_tree(p.first, tree, lease_path);
1029 }
1030 for (auto &&p : all_symlinks) {
1031 add_dir_to_tree(p.first, tree, lease_path);
1032 }
1033 int const row_count = static_cast<int>(tree.size());
1034 int const print_every = calculate_print_frequency(row_count);
1035 int curr_row = 0;
1036 LogCvmfs(kLogCvmfs, kLogStdout,
1037 "Changeset: %ld dirs, %ld files, %ld symlinks", tree.size(),
1038 all_files.size(), all_symlinks.size());
1039
1040 // STEP 2:
1041 // - process all the changes with DFS traversal
1042 // - make directories in pre-order
1043 // - add files/symlinks and schedule upload in post-order
1044 catalog_manager.SetupSingleCatalogUploadCallback();
1045 std::stack<string> dfs_stack;
1046 for (auto &&p : tree) {
1047 // figure out the starting point by checking whose parent is missing from
1048 // the tree
1049 if (p.first == "" || !tree.count(get_parent(p.first))) {
1050 CUSTOM_ASSERT(dfs_stack.empty(),
1051 "provided DB input forms more than one path trees");
1052 dfs_stack.push(p.first);
1053 }
1054 }
1055 std::set<string> visited;
1056 while (!dfs_stack.empty()) {
1057 string const curr_dir = dfs_stack.top();
1058 // add content for the dir in post-order traversal
1059 if (visited.count(curr_dir)) {
1060 curr_row++;
1061 if (all_symlinks.count(curr_dir)) {
1062 add_symlinks(catalog_manager, all_symlinks.at(curr_dir));
1063 }
1064 if (all_files.count(curr_dir)) {
1065 add_files(catalog_manager, all_files.at(curr_dir));
1066 }
1067 // snapshot the dir (if it's a nested catalog mountpoint)
1068 catalog::DirectoryEntry dir_entry;
1069 bool exists = false;
1070 exists = catalog_manager.LookupDirEntry(
1071 MakeCatalogPath(curr_dir), catalog::kLookupDefault, &dir_entry);
1072 CUSTOM_ASSERT(exists, "Directory %s is missing from the catalog",
1073 curr_dir.c_str());
1074 if (dir_entry.IsNestedCatalogMountpoint()
1075 || dir_entry.IsNestedCatalogRoot()) {
1076 catalog_manager.AddCatalogToQueue(curr_dir);
1077 catalog_manager.ScheduleReadyCatalogs();
1078 }
1079 dfs_stack.pop();
1080 SHOW_PROGRESS("directories", print_every, curr_row, row_count);
1081 } else {
1082 visited.insert(curr_dir);
1083 // push children to the stack
1084 auto it = tree.find(curr_dir);
1085 if (it != tree.end()) {
1086 for (auto &&child : it->second) {
1087 dfs_stack.push(child);
1088 }
1089 tree.erase(it);
1090 }
1091 if (!all_dirs.count(curr_dir))
1092 continue;
1093
1094 // create the dir first in pre-order traversal
1095 const IngestSQL::Directory &dir = all_dirs.at(curr_dir);
1096 catalog::DirectoryEntry dir_entry;
1097
1098 bool exists = false;
1099 exists = catalog_manager.LookupDirEntry(
1100 MakeCatalogPath(curr_dir), catalog::kLookupDefault, &dir_entry);
1101 CUSTOM_ASSERT(
1102 !(exists && !S_ISDIR(dir_entry.mode_)),
1103 "Refusing to replace existing file/symlink at %s with a directory",
1104 dir.name.c_str());
1105
1106 dir_entry.name_ = NameString(get_basename(dir.name));
1107 dir_entry.mtime_ = dir.mtime / 1000000000;
1108 dir_entry.mode_ = dir.mode | S_IFDIR;
1109 dir_entry.mode_ &= (S_IFDIR | 0777);
1110 dir_entry.uid_ = dir.owner;
1111 dir_entry.gid_ = dir.grp;
1112 dir_entry.has_xattrs_ = !dir.xattr.IsEmpty();
1113
1114 bool add_nested_catalog = false;
1115
1116 if (exists) {
1117 catalog_manager.TouchDirectory(dir_entry, dir.xattr, dir.name);
1118 if ((!dir_entry.IsNestedCatalogMountpoint()
1119 && !dir_entry.IsNestedCatalogRoot())
1120 && (g_add_missing_catalogs || dir.nested)) {
1121 add_nested_catalog = true;
1122 LogCvmfs(kLogCvmfs, kLogVerboseMsg,
1123 "Touching existing directory %s and adding nested catalog",
1124 dir.name.c_str());
1125 } else {
1126 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Touching existing directory %s",
1127 dir.name.c_str());
1128 }
1129 } else {
1130 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Adding directory [%s]",
1131 dir.name.c_str());
1132 catalog_manager.AddDirectory(dir_entry, dir.xattr,
1133 get_parent(dir.name));
1134 if (dir.nested) {
1135 add_nested_catalog = true;
1136 }
1137 }
1138 if (add_nested_catalog) {
1139 // now add a .cvmfscatalog file
1140 // so that manual changes won't remove the nested catalog
1141 LogCvmfs(kLogCvmfs, kLogVerboseMsg,
1142 "Placing .cvmfscatalog file in [%s]", dir.name.c_str());
1143 catalog::DirectoryEntryBase dir2;
1144 dir2.name_ = NameString(".cvmfscatalog");
1145 dir2.mtime_ = dir.mtime / 1000000000;
1146 dir2.mode_ = (S_IFREG | 0666);
1147 dir2.uid_ = 0;
1148 dir2.gid_ = 0;
1149 dir2.has_xattrs_ = 0;
1150 dir2.checksum_ = shash::MkFromHexPtr(
1151 shash::HexPtr("da39a3ee5e6b4b0d3255bfef95601890afd80709"),
1152 shash::kSuffixNone); // hash of ""
1153 XattrList const xattr2;
1154 catalog_manager.AddFile(dir2, xattr2, dir.name);
1155
1156 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Creating Nested Catalog [%s]",
1157 dir.name.c_str());
1158 catalog_manager.CreateNestedCatalog(dir.name);
1159 }
1160 }
1161 }
1162
1163 // sanity check that we have processed all the input
1164 CUSTOM_ASSERT(tree.empty(),
1165 "not all directories are processed, malformed input DB?");
1166 catalog_manager.RemoveSingleCatalogUploadCallback();
1167 return 0;
1168 }
1169
1170 int swissknife::IngestSQL::add_symlinks(
1171 catalog::WritableCatalogManager &catalog_manager,
1172 const std::vector<Symlink> &symlinks) {
1173 for (auto &&symlink : symlinks) {
1174 catalog::DirectoryEntry dir;
1175 catalog::DirectoryEntryBase dir2;
1176 XattrList const xattr;
1177 bool exists = false;
1178 exists = catalog_manager.LookupDirEntry(MakeCatalogPath(symlink.name),
1179 catalog::kLookupDefault, &dir);
1180
1181 dir2.name_ = NameString(get_basename(symlink.name));
1182 dir2.mtime_ = symlink.mtime / 1000000000;
1183 dir2.uid_ = symlink.owner;
1184 dir2.gid_ = symlink.grp;
1185 dir2.has_xattrs_ = false;
1186 dir2.symlink_ = LinkString(symlink.target);
1187 dir2.mode_ = S_IFLNK | 0777;
1188
1189 int noop = false;
1190
1191 if (exists) {
1192 if (symlink.skip_if_file_or_dir) {
1193 if (S_ISDIR(dir.mode_) || S_ISREG(dir.mode_)) {
1194 LogCvmfs(kLogCvmfs, kLogVerboseMsg,
1195 "File or directory for symlink [%s] exists, skipping "
1196 "symlink creation",
1197 symlink.name.c_str());
1198 noop = true;
1199 } else if (S_ISLNK(dir.mode_)) {
1200 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Removing existing symlink [%s]",
1201 symlink.name.c_str());
1202 catalog_manager.RemoveFile(symlink.name);
1203 } else {
1204 CUSTOM_ASSERT(0, "unknown mode for dirent: %d", dir.mode_);
1205 }
1206 } else {
1207 CUSTOM_ASSERT(!S_ISDIR(dir.mode_),
1208 "Not removing directory [%s] to create symlink",
1209 symlink.name.c_str());
1210 LogCvmfs(kLogCvmfs, kLogVerboseMsg,
1211 "Removing existing file/symlink [%s]", symlink.name.c_str());
1212 catalog_manager.RemoveFile(symlink.name);
1213 }
1214 }
1215 if (!noop) {
1216 string const parent = get_parent(symlink.name);
1217 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Adding symlink [%s] -> [%s]",
1218 symlink.name.c_str(), symlink.target.c_str());
1219 catalog_manager.AddFile(dir2, xattr, parent);
1220 }
1221 }
1222 return 0;
1223 }
1224
1225 static int check_hash(const char *hash) {
1226 if (strlen(hash) != 40) {
1227 return 1;
1228 }
1229 for (int i = 0; i < 40; i++) {
1230 // < '0' || > 'f' || ( > '9' && < 'a' )
1231 if (hash[i] < 0x30 || hash[i] > 0x66
1232 || (hash[i] > 0x39 && hash[i] < 0x61)) {
1233 return 1;
1234 }
1235 }
1236 return 0;
1237 }
1238
1239 bool check_prefix(const std::string &path, const std::string &prefix) {
1240 if (prefix == "" || prefix == "/") {
1241 return true;
1242 }
1243 if ("/" + path == prefix) {
1244 return true;
1245 }
1246 if (!HasPrefix(path, prefix, false)) {
1247 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Entry %s is outside lease path: %s",
1248 path.c_str(), prefix.c_str());
1249 return false;
1250 }
1251 return true;
1252 }
1253
1254 void swissknife::IngestSQL::load_dirs(
1255 sqlite3 *db, const std::string &lease_path,
1256 const std::string &additional_prefix,
1257 std::map<std::string, Directory> &all_dirs) {
1258 sqlite3_stmt *stmt;
1259 int const schema_revision = get_db_schema_revision(db);
1260 string select_stmt = "SELECT name, mode, mtime, owner, grp, acl, nested FROM "
1261 "dirs";
1262 if (schema_revision <= 3) {
1263 select_stmt = "SELECT name, mode, mtime, owner, grp, acl FROM dirs";
1264 }
1265 int const ret = sqlite3_prepare_v2(db, select_stmt.c_str(), -1, &stmt, NULL);
1266 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
1267 while (sqlite3_step(stmt) == SQLITE_ROW) {
1268 char *name_cstr = (char *)sqlite3_column_text(stmt, 0);
1269 mode_t const mode = sqlite3_column_int(stmt, 1);
1270 time_t const mtime = sqlite3_column_int64(stmt, 2);
1271 uid_t const owner = sqlite3_column_int(stmt, 3);
1272 gid_t const grp = sqlite3_column_int(stmt, 4);
1273 int const nested = schema_revision <= 3 ? 1 : sqlite3_column_int(stmt, 6);
1274
1275 string const name = additional_prefix + sanitise_name(name_cstr);
1276 CUSTOM_ASSERT(check_prefix(name, lease_path),
1277 "%s is not below lease path %s", name.c_str(),
1278 lease_path.c_str());
1279
1280 Directory dir(name, mtime, mode, owner, grp, nested);
1281 char *acl = (char *)sqlite3_column_text(stmt, 5);
1282 dir.xattr = marshal_xattrs(acl);
1283 all_dirs.insert(std::make_pair(name, dir));
1284 }
1285 CHECK_SQLITE_ERROR(sqlite3_finalize(stmt), SQLITE_OK);
1286 }
1287
1288 void swissknife::IngestSQL::load_files(
1289 sqlite3 *db, const std::string &lease_path,
1290 const std::string &additional_prefix,
1291 std::map<std::string, std::vector<File> > &all_files) {
1292 sqlite3_stmt *stmt;
1293 int const schema_revision = get_db_schema_revision(db);
1294 string select_stmt = "SELECT name, mode, mtime, owner, grp, size, hashes, "
1295 "internal, compressed FROM files";
1296 if (schema_revision <= 2) {
1297 select_stmt = "SELECT name, mode, mtime, owner, grp, size, hashes, "
1298 "internal FROM files";
1299 }
1300 int const ret = sqlite3_prepare_v2(db, select_stmt.c_str(), -1, &stmt, NULL);
1301 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
1302 while (sqlite3_step(stmt) == SQLITE_ROW) {
1303 char *name = (char *)sqlite3_column_text(stmt, 0);
1304 mode_t const mode = sqlite3_column_int(stmt, 1);
1305 time_t const mtime = sqlite3_column_int64(stmt, 2);
1306 uid_t const owner = sqlite3_column_int(stmt, 3);
1307 gid_t const grp = sqlite3_column_int(stmt, 4);
1308 size_t const size = sqlite3_column_int64(stmt, 5);
1309 char *hashes_cstr = (char *)sqlite3_column_text(stmt, 6);
1310 int const internal = sqlite3_column_int(stmt, 7);
1311 int const compressed = schema_revision <= 2 ? 0
1312 : sqlite3_column_int(stmt, 8);
1313
1314 string names = additional_prefix + sanitise_name(name);
1315 CUSTOM_ASSERT(check_prefix(names, lease_path),
1316 "%s is not below lease path %s", names.c_str(),
1317 lease_path.c_str());
1318 string const parent_dir = get_parent(names);
1319
1320 if (!all_files.count(parent_dir)) {
1321 all_files[parent_dir] = vector<swissknife::IngestSQL::File>();
1322 }
1323 all_files[parent_dir].emplace_back(std::move(names), mtime, size, owner,
1324 grp, mode, internal, compressed);
1325
1326 // tokenize hashes
1327 char *ref;
1328 char *tok;
1329 tok = strtok_r(hashes_cstr, ",", &ref);
1330 vector<off_t> offsets;
1331 vector<size_t> sizes;
1332 vector<shash::Any> hashes;
1333 off_t offset = 0;
1334
1335 CUSTOM_ASSERT(size >= 0, "file size cannot be negative [%s]",
1336 names.c_str());
1337 size_t const kChunkSize = internal ? kInternalChunkSize
1338 : kExternalChunkSize;
1339
1340 while (tok) {
1341 offsets.push_back(offset);
1342 // TODO: check the hash format
1343 CUSTOM_ASSERT(check_hash(tok) == 0,
1344 "provided hash for [%s] is invalid: %s", names.c_str(),
1345 tok);
1346 hashes.push_back(
1347 shash::MkFromHexPtr(shash::HexPtr(tok), shash::kSuffixNone));
1348 tok = strtok_r(NULL, ",", &ref);
1349 offset += kChunkSize; // in the future we might want variable chunk
1350 // sizes specified in the DB
1351 }
1352 size_t expected_num_chunks = size / kChunkSize;
1353 if (expected_num_chunks * (size_t)kChunkSize < (size_t)size || size == 0) {
1354 expected_num_chunks++;
1355 }
1356 CUSTOM_ASSERT(
1357 offsets.size() == expected_num_chunks,
1358 "offsets size %ld does not match expected number of chunks %ld",
1359 offsets.size(), expected_num_chunks);
1360 for (size_t i = 0; i < offsets.size() - 1; i++) {
1361 sizes.push_back(size_t(offsets[i + 1] - offsets[i]));
1362 }
1363
1364 sizes.push_back(size_t(size - offsets[offsets.size() - 1]));
1365 for (size_t i = 0; i < offsets.size(); i++) {
1366 FileChunk const chunk = FileChunk(hashes[i], offsets[i], sizes[i]);
1367 all_files[parent_dir].back().chunks.PushBack(chunk);
1368 }
1369 }
1370 CHECK_SQLITE_ERROR(sqlite3_finalize(stmt), SQLITE_OK);
1371 }
1372
1373 void swissknife::IngestSQL::load_symlinks(
1374 sqlite3 *db, const std::string &lease_path,
1375 const std::string &additional_prefix,
1376 std::map<std::string, std::vector<Symlink> > &all_symlinks) {
1377 sqlite3_stmt *stmt;
1378 string const select_stmt = "SELECT name, target, mtime, owner, grp, "
1379 "skip_if_file_or_dir FROM links";
1380 int const ret = sqlite3_prepare_v2(db, select_stmt.c_str(), -1, &stmt, NULL);
1381 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
1382 while (sqlite3_step(stmt) == SQLITE_ROW) {
1383 char *name_cstr = (char *)sqlite3_column_text(stmt, 0);
1384 char *target_cstr = (char *)sqlite3_column_text(stmt, 1);
1385 time_t const mtime = sqlite3_column_int64(stmt, 2);
1386 uid_t const owner = sqlite3_column_int(stmt, 3);
1387 gid_t const grp = sqlite3_column_int(stmt, 4);
1388 int const skip_if_file_or_dir = sqlite3_column_int(stmt, 5);
1389
1390 string names = additional_prefix + sanitise_name(name_cstr);
1391 CUSTOM_ASSERT(check_prefix(names, lease_path),
1392 "%s is not below lease path %s", names.c_str(),
1393 lease_path.c_str());
1394 string target = target_cstr;
1395 string const parent_dir = get_parent(names);
1396
1397 if (!all_symlinks.count(parent_dir)) {
1398 all_symlinks[parent_dir] = vector<swissknife::IngestSQL::Symlink>();
1399 }
1400 all_symlinks[parent_dir].emplace_back(std::move(names), std::move(target),
1401 mtime, owner, grp,
1402 skip_if_file_or_dir);
1403 }
1404 CHECK_SQLITE_ERROR(sqlite3_finalize(stmt), SQLITE_OK);
1405 }
1406
1407 int swissknife::IngestSQL::add_files(
1408 catalog::WritableCatalogManager &catalog_manager,
1409 const std::vector<File> &files) {
1410 for (auto &&file : files) {
1411 catalog::DirectoryEntry dir;
1412 XattrList const xattr;
1413 bool exists = false;
1414 exists = catalog_manager.LookupDirEntry(MakeCatalogPath(file.name),
1415 catalog::kLookupDefault, &dir);
1416
1417 dir.name_ = NameString(get_basename(file.name));
1418 dir.mtime_ = file.mtime / 1000000000;
1419 dir.mode_ = file.mode | S_IFREG;
1420 dir.mode_ &= (S_IFREG | 0777);
1421 dir.uid_ = file.owner;
1422 dir.gid_ = file.grp;
1423 dir.size_ = file.size;
1424 dir.has_xattrs_ = false;
1425 dir.is_external_file_ = !file.internal;
1426 dir.set_is_chunked_file(true);
1427 dir.checksum_ = shash::MkFromHexPtr(
1428 shash::HexPtr("0000000000000000000000000000000000000000"),
1429 shash::kSuffixNone);
1430
1431 // compression is permitted only for internal data
1432 CUSTOM_ASSERT(file.internal || (!file.internal && file.compressed < 2),
1433 "compression is only allowed for internal data [%s]",
1434 file.name.c_str());
1435
1436 switch (file.compressed) {
1437 case 1: // Uncompressed
1438 dir.compression_algorithm_ = zlib::kNoCompression;
1439 break;
1440 case 2: // Compressed with Zlib
1441 dir.compression_algorithm_ = zlib::kZlibDefault;
1442 break;
1443 // future cases: different compression schemes
1444 default: // default behaviour: compressed if internal, content-addressed.
1445 // Uncompressed if external
1446 dir.compression_algorithm_ = file.internal ? zlib::kZlibDefault
1447 : zlib::kNoCompression;
1448 }
1449
1450 if (exists) {
1451 CUSTOM_ASSERT(
1452 !S_ISDIR(dir.mode()) && !S_ISLNK(dir.mode()),
1453 "Refusing to replace existing dir/symlink at %s with a file",
1454 file.name.c_str());
1455 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Removing existing file [%s]",
1456 file.name.c_str());
1457 catalog_manager.RemoveFile(file.name);
1458 }
1459 string const parent = get_parent(file.name);
1460 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Adding chunked file [%s]",
1461 file.name.c_str());
1462 catalog_manager.AddChunkedFile(dir, xattr, parent, file.chunks);
1463 }
1464
1465 return 0;
1466 }
1467
1468 int swissknife::IngestSQL::do_deletions(
1469 sqlite3 *db, catalog::WritableCatalogManager &catalog_manager,
1470 const std::string &lease_path, const std::string &additional_prefix) {
1471 sqlite3_stmt *stmt;
1472 int const row_count = get_row_count(db, "deletions");
1473 int const print_every = calculate_print_frequency(row_count);
1474 int curr_row = 0;
1475 int ret = sqlite3_prepare_v2(db,
1476 "SELECT name, directory, file, link FROM "
1477 "deletions ORDER BY length(name) DESC",
1478 -1, &stmt, NULL);
1479 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
1480 while (sqlite3_step(stmt) == SQLITE_ROW) {
1481 curr_row++;
1482
1483 char *name = (char *)sqlite3_column_text(stmt, 0);
1484 int64_t const isdir = sqlite3_column_int64(stmt, 1);
1485 int64_t const isfile = sqlite3_column_int64(stmt, 2);
1486 int64_t const islink = sqlite3_column_int64(stmt, 3);
1487
1488 string const names = additional_prefix + sanitise_name(name);
1489 CUSTOM_ASSERT(check_prefix(names, lease_path),
1490 "%s is not below lease path %s", names.c_str(),
1491 lease_path.c_str());
1492
1493 catalog::DirectoryEntry dirent;
1494 bool exists = false;
1495 exists = catalog_manager.LookupDirEntry(MakeCatalogPath(names),
1496 catalog::kLookupDefault, &dirent);
1497 if (exists) {
1498 if ((isdir && S_ISDIR(dirent.mode()))
1499 || (islink && S_ISLNK(dirent.mode()))
1500 || (isfile && S_ISREG(dirent.mode()))) {
1501 if (S_ISDIR(dirent.mode())) {
1502 PathString names_path(names);
1503 recursively_delete_directory(names_path, catalog_manager);
1504 } else {
1505 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Removing link/file [%s]",
1506 names.c_str());
1507 catalog_manager.RemoveFile(names);
1508 }
1509 } else {
1510 LogCvmfs(kLogCvmfs, kLogVerboseMsg,
1511 "Mismatch in deletion type, not deleting: [%s] (dir %ld/%d , "
1512 "link %ld/%d, file %ld/%d)",
1513 names.c_str(), isdir, S_ISDIR(dirent.mode()), islink,
1514 S_ISLNK(dirent.mode()), isfile, S_ISREG(dirent.mode()));
1515 }
1516 } else {
1517 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Not Removing non-existent [%s]",
1518 names.c_str());
1519 }
1520
1521 SHOW_PROGRESS("deletions", print_every, curr_row, row_count);
1522 }
1523 ret = sqlite3_finalize(stmt);
1524 return ret;
1525 }
1526
1527 const char *schema[] = {"PRAGMA journal_mode=WAL;",
1528
1529 "CREATE TABLE IF NOT EXISTS dirs ( \
1530 name TEXT PRIMARY KEY, \
1531 mode INTEGER NOT NULL DEFAULT 493,\
1532 mtime INTEGER NOT NULL DEFAULT 0,\
1533 owner INTEGER NOT NULL DEFAULT 0, \
1534 grp INTEGER NOT NULL DEFAULT 0, \
1535 acl TEXT NOT NULL DEFAULT '', \
1536 nested INTEGER DEFAULT 1);",
1537
1538 "CREATE TABLE IF NOT EXISTS files ( \
1539 name TEXT PRIMARY KEY, \
1540 mode INTEGER NOT NULL DEFAULT 420, \
1541 mtime INTEGER NOT NULL DEFAULT 0,\
1542 owner INTEGER NOT NULL DEFAULT 0,\
1543 grp INTEGER NOT NULL DEFAULT 0,\
1544 size INTEGER NOT NULL DEFAULT 0,\
1545 hashes TEXT NOT NULL DEFAULT '',\
1546 internal INTEGER NOT NULL DEFAULT 0,\
1547 compressed INTEGER NOT NULL DEFAULT 0\
1548 );",
1549
1550 "CREATE TABLE IF NOT EXISTS links (\
1551 name TEXT PRIMARY KEY,\
1552 target TEXT NOT NULL DEFAULT '',\
1553 mtime INTEGER NOT NULL DEFAULT 0,\
1554 owner INTEGER NOT NULL DEFAULT 0,\
1555 grp INTEGER NOT NULL DEFAULT 0,\
1556 skip_if_file_or_dir INTEGER NOT NULL DEFAULT 0\
1557 );",
1558
1559 "CREATE TABLE IF NOT EXISTS deletions (\
1560 name TEXT PRIMARY KEY,\
1561 directory INTEGER NOT NULL DEFAULT 0,\
1562 file INTEGER NOT NULL DEFAULT 0,\
1563 link INTEGER NOT NULL DEFAULT 0\
1564 );",
1565
1566 "CREATE TABLE IF NOT EXISTS properties (\
1567 key TEXT PRIMARY KEY,\
1568 value TEXT NOT NULL\
1569 );",
1570
1571 "INSERT INTO properties VALUES ('schema_revision', "
1572 "'4') ON CONFLICT DO NOTHING;",
1573 NULL};
1574
1575 static void create_empty_database(string &filename) {
1576 sqlite3 *db_out;
1577 LogCvmfs(kLogCvmfs, kLogStdout, "Creating empty database file %s",
1578 filename.c_str());
1579 int ret = sqlite3_open_v2(filename.c_str(), &db_out,
1580 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
1581 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
1582 relax_db_locking(db_out);
1583
1584 const char **ptr = schema;
1585 while (*ptr != NULL) {
1586 ret = sqlite3_exec(db_out, *ptr, NULL, NULL, NULL);
1587 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
1588 ptr++;
1589 }
1590 sqlite3_close(db_out);
1591 }
1592
1593 static void recursively_delete_directory(
1594 PathString &path, catalog::WritableCatalogManager &catalog_manager) {
1595 catalog::DirectoryEntryList const listing;
1596
1597 // Add all names
1598 catalog::StatEntryList listing_from_catalog;
1599 bool const retval = catalog_manager.ListingStat(
1600 PathString("/" + path.ToString()), &listing_from_catalog);
1601
1602 CUSTOM_ASSERT(retval, "failed to call ListingStat for %s", path.c_str());
1603
1604 if (!catalog_manager.IsTransitionPoint(path.ToString())) {
1605 for (unsigned i = 0; i < listing_from_catalog.size(); ++i) {
1606 PathString entry_path;
1607 entry_path.Assign(path);
1608 entry_path.Append("/", 1);
1609 entry_path.Append(listing_from_catalog.AtPtr(i)->name.GetChars(),
1610 listing_from_catalog.AtPtr(i)->name.GetLength());
1611
1612 if (S_ISDIR(listing_from_catalog.AtPtr(i)->info.st_mode)) {
1613 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Recursing into %s/",
1614 entry_path.ToString().c_str());
1615 recursively_delete_directory(entry_path, catalog_manager);
1616
1617
1618 } else {
1619 LogCvmfs(kLogCvmfs, kLogVerboseMsg, " Recursively removing %s",
1620 entry_path.ToString().c_str());
1621 catalog_manager.RemoveFile(entry_path.ToString());
1622 }
1623 }
1624 } else {
1625 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Removing nested catalog %s",
1626 path.ToString().c_str());
1627 catalog_manager.RemoveNestedCatalog(path.ToString(), false);
1628 }
1629 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "Removing directory %s",
1630 path.ToString().c_str());
1631 catalog_manager.RemoveDirectory(path.ToString());
1632 }
1633
1634
1635 static void relax_db_locking(sqlite3 *db) {
1636 int ret = 0;
1637 ret = sqlite3_exec(db, "PRAGMA temp_store=2", NULL, NULL, NULL);
1638 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
1639 ret = sqlite3_exec(db, "PRAGMA synchronous=OFF", NULL, NULL, NULL);
1640 CHECK_SQLITE_ERROR(ret, SQLITE_OK);
1641 }
1642
1643
1644 extern "C" void *lease_refresh_thread(void *payload) {
1645 while (!g_stop_refresh) {
1646 sleep(2);
1647 refresh_lease();
1648 }
1649 return NULL;
1650 }
1651
1652 static bool isDatabaseMarkedComplete(const char *dbfile) {
1653 int ret;
1654 sqlite3 *db;
1655 sqlite3_stmt *stmt;
1656 bool retval = false;
1657
1658 ret = sqlite3_open(dbfile, &db);
1659 if (ret != SQLITE_OK) {
1660 return false;
1661 }
1662
1663 const char *req = "SELECT value FROM properties WHERE key='completed_graft'";
1664 ret = sqlite3_prepare_v2(db, req, -1, &stmt, NULL);
1665 if (ret != SQLITE_OK) {
1666 return false;
1667 }
1668 if (sqlite3_step(stmt) == SQLITE_ROW) {
1669 int const id = sqlite3_column_int(stmt, 0);
1670 if (id > 0) {
1671 retval = true;
1672 }
1673 }
1674 sqlite3_close(db);
1675 return retval;
1676 }
1677
1678 static void setDatabaseMarkedComplete(const char *dbfile) {
1679 int ret;
1680 sqlite3 *db;
1681 char *err;
1682
1683 ret = sqlite3_open(dbfile, &db);
1684 if (ret != SQLITE_OK) {
1685 return;
1686 }
1687
1688 const char *req = "INSERT INTO properties (key, value) VALUES "
1689 "('completed_graft',1) ON CONFLICT(key) DO UPDATE SET "
1690 "value=1 WHERE key='completed_graft'";
1691
1692 ret = sqlite3_exec(db, req, 0, 0, &err);
1693 if (ret != SQLITE_OK) {
1694 return;
1695 }
1696 sqlite3_close(db);
1697 }
1698
1699 static void invalidate_manifest(std::string proxy_list, std::string url) {
1700 // split the proxy string -- remove any '"' and split on '|' or ';'
1701 size_t pos = 0;
1702 // replace any ';' with '|' to simplify subsequent split
1703 while ((pos = proxy_list.find(';', pos)) != std::string::npos) {
1704 proxy_list.replace(pos, 1, 1, '|');
1705 ++pos;
1706 }
1707 // remove any leading or trailing '"'
1708 if (HasPrefix(proxy_list, "\"", true)) {
1709 proxy_list = proxy_list.substr(1);
1710 }
1711 if (HasSuffix(proxy_list, "\"", true)) {
1712 proxy_list = proxy_list.substr(0, proxy_list.size() - 1);
1713 }
1714 // rewrite the port from 6086 to 6081 to ensure the invalidation works
1715 replaceAllSubstrings(proxy_list, ":6086", ":6081");
1716
1717 std::vector<std::string> proxies = SplitString(proxy_list, '|');
1718
1719 // now iterate over all the proxies
1720 // for the first, force a no-cache GET back to google
1721 // for the remainder just PURGE
1722 bool first = true;
1723 for (auto p = proxies.begin(); p != proxies.end(); p++) {
1724 bool ok = true;
1725 const string proxy = *p;
1726 CURL *curl = NULL;
1727 CURLcode res = CURLE_OK;
1728 struct curl_slist *headers = NULL;
1729 curl = curl_easy_init();
1730 if (!curl) {
1731 LogCvmfs(kLogCvmfs, kLogStdout, "Unable to init curl!");
1732 return;
1733 }
1734 res = curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
1735 if (proxy != "DIRECT") {
1736 res = curl_easy_setopt(curl, CURLOPT_PROXY, proxy.c_str());
1737 }
1738 res = curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 1l);
1739 res = curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3l);
1740 res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunction);
1741
1742 if (first) {
1743 headers = curl_slist_append(headers, "Cache-Control: no-cache");
1744 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
1745 } else {
1746 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PURGE");
1747 }
1748
1749 res = curl_easy_perform(curl);
1750 if (res != CURLE_OK) {
1751 LogCvmfs(kLogCvmfs, kLogStdout,
1752 "Manifest invalidation failed: curl error = [%d] [%s] url = %s "
1753 "proxy = %s",
1754 res, curl_easy_strerror(res), url.c_str(), proxy.c_str());
1755 ok = false;
1756 }
1757 if (headers) {
1758 curl_slist_free_all(headers);
1759 }
1760 curl_easy_cleanup(curl);
1761 if (ok) {
1762 first = false;
1763 }
1764 }
1765 }
1766
1767
1768 static void wait_for_update(std::string path, long revision) {
1769 char val[101];
1770 memset(val, 0, 101);
1771 long current = -1;
1772 DIR *d;
1773 while (-1 != getxattr(path.c_str(), "user.revision", val, 100)) {
1774 const long x = atol(val);
1775 if (x >= revision) {
1776 LogCvmfs(kLogCvmfs, kLogStdout, "Mount reached revision %ld", x);
1777 return;
1778 } else if (x != current) {
1779 current = x;
1780 LogCvmfs(kLogCvmfs, kLogStdout, "Mount at revision %ld, waiting..", x);
1781 }
1782 sleep(1);
1783 d = opendir(path.c_str());
1784 if (d) {
1785 closedir(d);
1786 }
1787 }
1788 LogCvmfs(kLogCvmfs, kLogStdout,
1789 "Unable to query user.revision xattr of [%s]: errno: %d",
1790 path.c_str(), errno);
1791 }
1792