GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/swissknife_sync.cc
Date: 2026-03-22 02:40:38
Exec Total Coverage
Lines: 0 495 0.0%
Branches: 0 322 0.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System
3 *
4 * This tool figures out the changes made to a cvmfs repository by means
5 * of a union file system mounted on top of a cvmfs volume.
6 * We take all three volumes (namely union, overlay and repository) into
7 * account to sync the changes back into the repository.
8 *
9 * On the repository side we have a catalogs directory that mimics the
10 * shadow directory structure and stores compressed and uncompressed
11 * versions of all catalogs. The raw data are stored in the data
12 * subdirectory in zlib-compressed form. They are named with their SHA-1
13 * hash of the compressed file (like in CVMFS client cache, but with a
14 * 2-level cache hierarchy). Symlinks from the catalog directory to the
15 * data directory form the connection. If necessary, add a .htaccess file
16 * to allow Apache to follow the symlinks.
17 */
18
19 // NOLINTNEXTLINE
20 #define _FILE_OFFSET_BITS 64
21 // NOLINTNEXTLINE
22 #define __STDC_FORMAT_MACROS
23
24 #include "swissknife_sync.h"
25
26 #include <errno.h>
27 #include <fcntl.h>
28 #include <glob.h>
29 #include <inttypes.h>
30 #include <limits.h>
31
32 #include <cstdio>
33 #include <cstdlib>
34 #include <string>
35 #include <vector>
36
37 #include "catalog_mgr_ro.h"
38 #include "catalog_mgr_rw.h"
39 #include "catalog_virtual.h"
40 #include "manifest.h"
41 #include "monitor.h"
42 #include "network/download.h"
43 #include "path_filters/dirtab.h"
44 #include "reflog.h"
45 #include "sanitizer.h"
46 #include "statistics.h"
47 #include "statistics_database.h"
48 #include "sync_mediator.h"
49 #include "sync_union.h"
50 #include "sync_union_aufs.h"
51 #include "sync_union_overlayfs.h"
52 #include "util/capabilities.h"
53 #include "util/logging.h"
54 #include "util/platform.h"
55 #include "util/string.h"
56
57 using namespace std; // NOLINT
58
59 bool swissknife::CommandSync::CheckParams(const SyncParameters &p) {
60 if (!DirectoryExists(p.dir_scratch)) {
61 PrintError("overlay (copy on write) directory does not exist");
62 return false;
63 }
64 if (!DirectoryExists(p.dir_union)) {
65 PrintError("union volume does not exist");
66 return false;
67 }
68 if (!DirectoryExists(p.dir_rdonly)) {
69 PrintError("cvmfs read/only repository does not exist");
70 return false;
71 }
72 if (p.stratum0 == "") {
73 PrintError("Stratum0 url missing");
74 return false;
75 }
76
77 if (p.manifest_path == "") {
78 PrintError("manifest output required");
79 return false;
80 }
81 if (!DirectoryExists(p.dir_temp)) {
82 PrintError("data store directory does not exist");
83 return false;
84 }
85
86 if (p.min_file_chunk_size >= p.avg_file_chunk_size
87 || p.avg_file_chunk_size >= p.max_file_chunk_size) {
88 PrintError("file chunk size values are not sane");
89 return false;
90 }
91
92 if (HasPrefix(p.spooler_definition, "gw", false)) {
93 if (p.session_token_file.empty()) {
94 PrintError("Session token file has to be provided "
95 "when upstream type is gw.");
96 return false;
97 }
98 }
99
100 return true;
101 }
102
103 int swissknife::CommandCreate::Main(const swissknife::ArgumentList &args) {
104 const string manifest_path = *args.find('o')->second;
105 const string dir_temp = *args.find('t')->second;
106 const string spooler_definition = *args.find('r')->second;
107 const string repo_name = *args.find('n')->second;
108 const string reflog_chksum_path = *args.find('R')->second;
109 if (args.find('l') != args.end()) {
110 const unsigned log_level = kLogLevel0
111 << String2Uint64(*args.find('l')->second);
112 if (log_level > kLogNone) {
113 LogCvmfs(kLogCvmfs, kLogStderr, "invalid log level");
114 return 1;
115 }
116 SetLogVerbosity(static_cast<LogLevels>(log_level));
117 }
118 shash::Algorithms hash_algorithm = shash::kSha1;
119 if (args.find('a') != args.end()) {
120 hash_algorithm = shash::ParseHashAlgorithm(*args.find('a')->second);
121 if (hash_algorithm == shash::kAny) {
122 PrintError("unknown hash algorithm");
123 return 1;
124 }
125 }
126
127 const bool volatile_content = (args.count('v') > 0);
128 const bool garbage_collectable = (args.count('z') > 0);
129 std::string voms_authz;
130 if (args.find('V') != args.end()) {
131 voms_authz = *args.find('V')->second;
132 }
133
134 const upload::SpoolerDefinition sd(spooler_definition, hash_algorithm,
135 zlib::kZlibDefault);
136 const UniquePtr<upload::Spooler> spooler(upload::Spooler::Construct(sd));
137 assert(spooler.IsValid());
138
139 const UniquePtr<manifest::Manifest> manifest(
140 catalog::WritableCatalogManager::CreateRepository(
141 dir_temp, volatile_content, voms_authz, spooler.weak_ref()));
142 if (!manifest.IsValid()) {
143 PrintError("Swissknife Sync: Failed to create new repository");
144 return 1;
145 }
146
147 UniquePtr<manifest::Reflog> reflog(CreateEmptyReflog(dir_temp, repo_name));
148 if (!reflog.IsValid()) {
149 PrintError("Swissknife Sync: Failed to create fresh Reflog");
150 return 1;
151 }
152
153 reflog->DropDatabaseFileOwnership();
154 const string reflog_path = reflog->database_file();
155 reflog.Destroy();
156 shash::Any reflog_hash(hash_algorithm);
157 manifest::Reflog::HashDatabase(reflog_path, &reflog_hash);
158 spooler->UploadReflog(reflog_path);
159 spooler->WaitForUpload();
160 unlink(reflog_path.c_str());
161 if (spooler->GetNumberOfErrors()) {
162 LogCvmfs(kLogCvmfs, kLogStderr, "Swissknife Sync: Failed to upload reflog");
163 return 4;
164 }
165 assert(!reflog_chksum_path.empty());
166 manifest::Reflog::WriteChecksum(reflog_chksum_path, reflog_hash);
167
168 // set optional manifest fields
169 const bool needs_bootstrap_shortcuts = !voms_authz.empty();
170 manifest->set_garbage_collectability(garbage_collectable);
171 manifest->set_has_alt_catalog_path(needs_bootstrap_shortcuts);
172
173 if (!manifest->Export(manifest_path)) {
174 PrintError("Swissknife Sync: Failed to create new repository");
175 return 5;
176 }
177
178 return 0;
179 }
180
181 int swissknife::CommandUpload::Main(const swissknife::ArgumentList &args) {
182 const string source = *args.find('i')->second;
183 const string dest = *args.find('o')->second;
184 const string spooler_definition = *args.find('r')->second;
185 shash::Algorithms hash_algorithm = shash::kSha1;
186 if (args.find('a') != args.end()) {
187 hash_algorithm = shash::ParseHashAlgorithm(*args.find('a')->second);
188 if (hash_algorithm == shash::kAny) {
189 PrintError("Swissknife Sync: Unknown hash algorithm");
190 return 1;
191 }
192 }
193
194 const upload::SpoolerDefinition sd(spooler_definition, hash_algorithm);
195 upload::Spooler *spooler = upload::Spooler::Construct(sd);
196 assert(spooler);
197 spooler->Upload(source, dest);
198 spooler->WaitForUpload();
199
200 if (spooler->GetNumberOfErrors() > 0) {
201 LogCvmfs(kLogCatalog, kLogStderr, "Swissknife Sync: failed to upload %s",
202 source.c_str());
203 return 1;
204 }
205
206 delete spooler;
207
208 return 0;
209 }
210
211 int swissknife::CommandPeek::Main(const swissknife::ArgumentList &args) {
212 const string file_to_peek = *args.find('d')->second;
213 const string spooler_definition = *args.find('r')->second;
214
215 // Hash doesn't matter
216 const upload::SpoolerDefinition sd(spooler_definition, shash::kAny);
217 upload::Spooler *spooler = upload::Spooler::Construct(sd);
218 assert(spooler);
219 const bool success = spooler->Peek(file_to_peek);
220
221 if (spooler->GetNumberOfErrors() > 0) {
222 LogCvmfs(kLogCatalog, kLogStderr, "Swissknife Sync: failed to peek for %s",
223 file_to_peek.c_str());
224 return 2;
225 }
226 if (!success) {
227 LogCvmfs(kLogCatalog, kLogStdout, "Swissknife Sync: %s not found",
228 file_to_peek.c_str());
229 return 1;
230 }
231 LogCvmfs(kLogCatalog, kLogStdout, "Swissknife Sync: %s available",
232 file_to_peek.c_str());
233
234 delete spooler;
235
236 return 0;
237 }
238
239 int swissknife::CommandRemove::Main(const ArgumentList &args) {
240 const string file_to_delete = *args.find('o')->second;
241 const string spooler_definition = *args.find('r')->second;
242
243 // Hash doesn't matter
244 const upload::SpoolerDefinition sd(spooler_definition, shash::kAny);
245 upload::Spooler *spooler = upload::Spooler::Construct(sd);
246 assert(spooler);
247 spooler->RemoveAsync(file_to_delete);
248 spooler->WaitForUpload();
249
250 if (spooler->GetNumberOfErrors() > 0) {
251 LogCvmfs(kLogCatalog, kLogStderr, "Swissknife Sync: failed to delete %s",
252 file_to_delete.c_str());
253 return 1;
254 }
255
256 delete spooler;
257
258 return 0;
259 }
260
261 int swissknife::CommandApplyDirtab::Main(const ArgumentList &args) {
262 const string dirtab_file = *args.find('d')->second;
263 union_dir_ = MakeCanonicalPath(*args.find('u')->second);
264 scratch_dir_ = MakeCanonicalPath(*args.find('s')->second);
265 const shash::Any base_hash = shash::MkFromHexPtr(
266 shash::HexPtr(*args.find('b')->second), shash::kSuffixCatalog);
267 const string stratum0 = *args.find('w')->second;
268 const string dir_temp = *args.find('t')->second;
269 verbose_ = (args.find('x') != args.end());
270
271 // check if there is a dirtab file
272 if (!FileExists(dirtab_file)) {
273 LogCvmfs(kLogCatalog, kLogVerboseMsg,
274 "Swissknife Sync: Didn't find a dirtab at '%s'. Skipping...",
275 dirtab_file.c_str());
276 return 0;
277 }
278
279 // parse dirtab file
280 catalog::Dirtab *dirtab = catalog::Dirtab::Create(dirtab_file);
281 if (!dirtab->IsValid()) {
282 LogCvmfs(kLogCatalog, kLogStderr,
283 "Swissknife Sync: Invalid or not readable dirtab '%s'",
284 dirtab_file.c_str());
285 return 1;
286 }
287 LogCvmfs(kLogCatalog, kLogVerboseMsg,
288 "Swissknife Sync: Found %lu rules in dirtab '%s'",
289 dirtab->RuleCount(), dirtab_file.c_str());
290
291 // initialize catalog infrastructure
292 const bool auto_manage_catalog_files = true;
293 const bool follow_redirects = (args.count('L') > 0);
294 const string proxy = (args.count('@') > 0) ? *args.find('@')->second : "";
295 if (!InitDownloadManager(follow_redirects, proxy)) {
296 return 1;
297 }
298 catalog::SimpleCatalogManager catalog_manager(
299 base_hash, stratum0, dir_temp, download_manager(), statistics(),
300 auto_manage_catalog_files);
301 catalog_manager.Init();
302
303 vector<string> new_nested_catalogs;
304 DetermineNestedCatalogCandidates(*dirtab, &catalog_manager,
305 &new_nested_catalogs);
306 const bool success = CreateCatalogMarkers(new_nested_catalogs);
307 delete dirtab;
308
309 return (success) ? 0 : 1;
310 }
311
312
313 namespace {
314
315 // Overwrite directory traversal in the globbing in order to avoid breaking out
316 // the repository tree
317
318 std::string *g_glob_uniondir = NULL;
319
320 bool GlobCheckPath(const char *name) {
321 char resolved_cstr[PATH_MAX];
322 char *retval = realpath(name, resolved_cstr);
323 if (retval == NULL)
324 return false;
325
326 const std::string resolved(resolved_cstr);
327 if (resolved == *g_glob_uniondir)
328 return true;
329 if (!HasPrefix(resolved, (*g_glob_uniondir) + "/", false /*ignore_case*/)) {
330 errno = EACCES;
331 return false;
332 }
333 return true;
334 }
335
336 void *GlobOpendir(const char *name) {
337 if (!GlobCheckPath(name))
338 return NULL;
339 return opendir(name);
340 }
341
342 void GlobClosedir(void *dirp) { closedir(static_cast<DIR *>(dirp)); }
343
344 struct dirent *GlobReaddir(void *dirp) {
345 return readdir(static_cast<DIR *>(dirp));
346 }
347
348 int GlobLstat(const char *name, struct stat *st) {
349 if (!GlobCheckPath(name))
350 return -1;
351 return lstat(name, st);
352 }
353
354 int GlobStat(const char *name, struct stat *st) {
355 if (!GlobCheckPath(name))
356 return -1;
357 return stat(name, st);
358 }
359
360
361 } // anonymous namespace
362
363 void swissknife::CommandApplyDirtab::DetermineNestedCatalogCandidates(
364 const catalog::Dirtab &dirtab,
365 catalog::SimpleCatalogManager *catalog_manager,
366 vector<string> *nested_catalog_candidates) {
367 // find possible new nested catalog locations
368 const catalog::Dirtab::Rules &lookup_rules = dirtab.positive_rules();
369 catalog::Dirtab::Rules::const_iterator i = lookup_rules.begin();
370 const catalog::Dirtab::Rules::const_iterator iend = lookup_rules.end();
371 for (; i != iend; ++i) {
372 assert(!i->is_negation);
373
374 // run a glob using the current dirtab rule on the current repository
375 // state
376 const std::string &glob_string = i->pathspec.GetGlobString();
377 const std::string &glob_string_abs = union_dir_ + glob_string;
378 const int glob_flags = GLOB_ONLYDIR | GLOB_NOSORT | GLOB_PERIOD
379 | GLOB_ALTDIRFUNC;
380 glob_t glob_res;
381 g_glob_uniondir = new std::string(union_dir_);
382 glob_res.gl_opendir = GlobOpendir;
383 glob_res.gl_readdir = GlobReaddir;
384 glob_res.gl_closedir = GlobClosedir;
385 glob_res.gl_lstat = GlobLstat;
386 glob_res.gl_stat = GlobStat;
387 const int glob_retval = glob(glob_string_abs.c_str(), glob_flags, NULL,
388 &glob_res);
389 delete g_glob_uniondir;
390 g_glob_uniondir = NULL;
391
392 if (glob_retval == 0) {
393 // found some candidates... filtering by cvmfs catalog structure
394 LogCvmfs(kLogCatalog, kLogDebug,
395 "Swissknife Sync: Found %lu entries for pathspec (%s)",
396 glob_res.gl_pathc, glob_string.c_str());
397 FilterCandidatesFromGlobResult(dirtab, glob_res.gl_pathv,
398 glob_res.gl_pathc, catalog_manager,
399 nested_catalog_candidates);
400 } else if (glob_retval == GLOB_NOMATCH) {
401 LogCvmfs(kLogCvmfs, kLogStderr,
402 "Swissknife Sync: WARNING: cannot apply pathspec %s",
403 glob_string.c_str());
404 } else {
405 LogCvmfs(kLogCvmfs, kLogStderr,
406 "Swissknife Sync: Failed to run glob matching (%s)",
407 glob_string.c_str());
408 }
409
410 globfree(&glob_res);
411 }
412 }
413
414 void swissknife::CommandApplyDirtab::FilterCandidatesFromGlobResult(
415 const catalog::Dirtab &dirtab, char **paths, const size_t npaths,
416 catalog::SimpleCatalogManager *catalog_manager,
417 std::vector<std::string> *nested_catalog_candidates) {
418 // go through the paths produced by glob() and filter them
419 for (size_t i = 0; i < npaths; ++i) {
420 // process candidate paths
421 const std::string candidate(paths[i]);
422 const std::string candidate_rel = candidate.substr(union_dir_.size());
423
424 // check if path points to a directory
425 platform_stat64 candidate_info;
426 const int lstat_retval = platform_lstat(candidate.c_str(), &candidate_info);
427 if (lstat_retval != 0) {
428 LogCvmfs(kLogCatalog, kLogDebug | kLogStderr | kLogSyslogErr,
429 "Swissknife Sync: "
430 "Error in processing .cvmfsdirtab: cannot access %s (%d)",
431 candidate.c_str(), errno);
432 abort();
433 }
434 assert(lstat_retval == 0);
435 if (!S_ISDIR(candidate_info.st_mode)) {
436 // The GLOB_ONLYDIR flag is only a hint, non-directories can still be
437 // returned
438 LogCvmfs(kLogCatalog, kLogDebug,
439 "Swissknife Sync: "
440 "The '%s' dirtab entry does not point to a directory "
441 "but to a file or a symbolic link",
442 candidate_rel.c_str());
443 continue;
444 }
445
446 // check if the path is a meta-directory (. or ..)
447 assert(candidate_rel.size() >= 2);
448 if (candidate_rel.substr(candidate_rel.size() - 2) == "/."
449 || candidate_rel.substr(candidate_rel.size() - 3) == "/..") {
450 continue;
451 }
452
453 // check that the path isn't excluded in the dirtab
454 if (dirtab.IsOpposing(candidate_rel)) {
455 LogCvmfs(kLogCatalog, kLogDebug,
456 "Swissknife Sync: Candidate '%s' is excluded by dirtab",
457 candidate_rel.c_str());
458 continue;
459 }
460
461 // lookup the path in the catalog structure to find out if it already
462 // points to a nested catalog transition point. Furthermore it could be
463 // a new directory and thus not in any catalog yet.
464 catalog::DirectoryEntry dirent;
465 const bool lookup_success = catalog_manager->LookupPath(
466 candidate_rel, catalog::kLookupDefault, &dirent);
467 if (!lookup_success) {
468 LogCvmfs(kLogCatalog, kLogDebug,
469 "Swissknife Sync: Didn't find '%s' in catalogs, could "
470 "be a new directory and nested catalog.",
471 candidate_rel.c_str());
472 nested_catalog_candidates->push_back(candidate);
473 } else if (!dirent.IsNestedCatalogMountpoint()
474 && !dirent.IsNestedCatalogRoot()) {
475 LogCvmfs(kLogCatalog, kLogDebug,
476 "Swissknife Sync: Found '%s' in catalogs but is not a "
477 "nested catalog yet.",
478 candidate_rel.c_str());
479 nested_catalog_candidates->push_back(candidate);
480 } else {
481 // check if the nested catalog marker is still there, we might need to
482 // recreate the catalog after manual marker removal
483 // Note: First we check if the parent directory shows up in the scratch
484 // space to verify that it was touched (copy-on-write)
485 // Otherwise we would force the cvmfs client behind the union
486 // file-
487 // system to (potentially) unnecessarily fetch catalogs
488 if (DirectoryExists(scratch_dir_ + candidate_rel)
489 && !FileExists(union_dir_ + candidate_rel + "/.cvmfscatalog")) {
490 LogCvmfs(kLogCatalog, kLogStdout,
491 "Swissknife Sync: WARNING: '%s' should be a nested "
492 "catalog according to the dirtab. "
493 "Recreating...",
494 candidate_rel.c_str());
495 nested_catalog_candidates->push_back(candidate);
496 } else {
497 LogCvmfs(kLogCatalog, kLogDebug,
498 "Swissknife Sync: "
499 "Found '%s' in catalogs and it already is a nested catalog.",
500 candidate_rel.c_str());
501 }
502 }
503 }
504 }
505
506 bool swissknife::CommandApplyDirtab::CreateCatalogMarkers(
507 const std::vector<std::string> &new_nested_catalogs) {
508 // go through the new nested catalog paths and create .cvmfscatalog markers
509 // where necessary
510 bool success = true;
511 std::vector<std::string>::const_iterator k = new_nested_catalogs.begin();
512 const std::vector<std::string>::const_iterator kend = new_nested_catalogs
513 .end();
514 for (; k != kend; ++k) {
515 assert(!k->empty() && k->size() > union_dir_.size());
516
517 // was the marker already created by hand?
518 const std::string marker_path = *k + "/.cvmfscatalog";
519 if (FileExists(marker_path)) {
520 continue;
521 }
522
523 // create a nested catalog marker
524 const mode_t mode = kDefaultFileMode;
525 const int fd = open(marker_path.c_str(), O_CREAT, mode);
526 if (fd < 0) {
527 LogCvmfs(kLogCvmfs, kLogStderr,
528 "Swissknife Sync: Failed to create nested catalog marker "
529 "at '%s' (errno: %d)",
530 marker_path.c_str(), errno);
531 success = false;
532 continue;
533 }
534 close(fd);
535
536 // inform the user if requested
537 if (verbose_) {
538 LogCvmfs(kLogCvmfs, kLogStdout,
539 "Swissknife Sync: Auto-creating nested catalog in %s",
540 k->c_str());
541 }
542 }
543
544 return success;
545 }
546
547 struct chunk_arg {
548 chunk_arg(char param, size_t *save_to) : param(param), save_to(save_to) { }
549 char param;
550 size_t *save_to;
551 };
552
553 bool swissknife::CommandSync::ReadFileChunkingArgs(
554 const swissknife::ArgumentList &args, SyncParameters *params) {
555 typedef std::vector<chunk_arg> ChunkArgs;
556
557 // define where to store the value of which file chunk argument
558 ChunkArgs chunk_args;
559 chunk_args.push_back(chunk_arg('a', &params->avg_file_chunk_size));
560 chunk_args.push_back(chunk_arg('l', &params->min_file_chunk_size));
561 chunk_args.push_back(chunk_arg('h', &params->max_file_chunk_size));
562
563 // read the arguments
564 ChunkArgs::const_iterator i = chunk_args.begin();
565 const ChunkArgs::const_iterator iend = chunk_args.end();
566 for (; i != iend; ++i) {
567 const swissknife::ArgumentList::const_iterator arg = args.find(i->param);
568
569 if (arg != args.end()) {
570 const size_t arg_value = static_cast<size_t>(String2Uint64(*arg->second));
571 if (arg_value > 0) {
572 *i->save_to = arg_value;
573 } else {
574 return false;
575 }
576 }
577 }
578
579 // check if argument values are sane
580 return true;
581 }
582
583 int swissknife::CommandSync::Main(const swissknife::ArgumentList &args) {
584 const string start_time = GetGMTimestamp();
585
586 // Spawn monitoring process (watchdog)
587 const std::string watchdog_dir = "/tmp";
588 char watchdog_path[PATH_MAX];
589 const std::string timestamp = GetGMTimestamp("%Y.%m.%d-%H.%M.%S");
590 const int path_size = snprintf(watchdog_path, sizeof(watchdog_path),
591 "%s/cvmfs-swissknife-sync-stacktrace.%s.%d",
592 watchdog_dir.c_str(), timestamp.c_str(),
593 getpid());
594 assert(path_size > 0);
595 assert(path_size < PATH_MAX);
596 const UniquePtr<Watchdog> watchdog(Watchdog::Create(NULL, false /* needs_read_environ */));
597 watchdog->Spawn(std::string(watchdog_path));
598
599 SyncParameters params;
600
601 // Initialization
602 params.dir_union = MakeCanonicalPath(*args.find('u')->second);
603 params.dir_scratch = MakeCanonicalPath(*args.find('s')->second);
604 params.dir_rdonly = MakeCanonicalPath(*args.find('c')->second);
605 params.dir_temp = MakeCanonicalPath(*args.find('t')->second);
606 params.base_hash = shash::MkFromHexPtr(shash::HexPtr(*args.find('b')->second),
607 shash::kSuffixCatalog);
608 params.stratum0 = *args.find('w')->second;
609 params.manifest_path = *args.find('o')->second;
610 params.spooler_definition = *args.find('r')->second;
611
612 params.public_keys = *args.find('K')->second;
613 params.repo_name = *args.find('N')->second;
614
615 params.ttl_seconds = catalog::Catalog::kDefaultTTL;
616
617 if (args.find('f') != args.end())
618 params.union_fs_type = *args.find('f')->second;
619 if (args.find('A') != args.end())
620 params.is_balanced = true;
621 if (args.find('x') != args.end())
622 params.print_changeset = true;
623 if (args.find('y') != args.end())
624 params.dry_run = true;
625 if (args.find('m') != args.end())
626 params.mucatalogs = true;
627 if (args.find('i') != args.end())
628 params.ignore_xdir_hardlinks = true;
629 if (args.find('d') != args.end())
630 params.stop_for_catalog_tweaks = true;
631 if (args.find('V') != args.end())
632 params.voms_authz = true;
633 if (args.find('F') != args.end())
634 params.authz_file = *args.find('F')->second;
635 if (args.find('k') != args.end())
636 params.include_xattrs = true;
637 if (args.find('j') != args.end())
638 params.enable_mtime_ns = true;
639 if (args.find('Y') != args.end())
640 params.external_data = true;
641 if (args.find('W') != args.end())
642 params.direct_io = true;
643 if (args.find('S') != args.end()) {
644 const bool retval = catalog::VirtualCatalog::ParseActions(
645 *args.find('S')->second, &params.virtual_dir_actions);
646 if (!retval) {
647 LogCvmfs(kLogCvmfs, kLogStderr,
648 "Swissknife Sync: Invalid virtual catalog options: %s",
649 args.find('S')->second->c_str());
650 return 1;
651 }
652 }
653 if (args.find('z') != args.end()) {
654 const unsigned log_level = 1 << (kLogLevel0
655 + String2Uint64(*args.find('z')->second));
656 if (log_level > kLogNone) {
657 LogCvmfs(kLogCvmfs, kLogStderr, "Swissknife Sync: invalid log level");
658 return 1;
659 }
660 SetLogVerbosity(static_cast<LogLevels>(log_level));
661 }
662
663 if (args.find('X') != args.end())
664 params.max_weight = String2Uint64(*args.find('X')->second);
665 if (args.find('M') != args.end())
666 params.min_weight = String2Uint64(*args.find('M')->second);
667
668 if (args.find('p') != args.end()) {
669 params.use_file_chunking = true;
670 if (!ReadFileChunkingArgs(args, &params)) {
671 PrintError("Swissknife Sync: Failed to read file chunk size values");
672 return 2;
673 }
674 }
675 if (args.find('O') != args.end()) {
676 params.generate_legacy_bulk_chunks = true;
677 }
678 shash::Algorithms hash_algorithm = shash::kSha1;
679 if (args.find('e') != args.end()) {
680 hash_algorithm = shash::ParseHashAlgorithm(*args.find('e')->second);
681 if (hash_algorithm == shash::kAny) {
682 PrintError("Swissknife Sync: Unknown hash algorithm");
683 return 1;
684 }
685 }
686 if (args.find('Z') != args.end()) {
687 params.compression_alg = zlib::ParseCompressionAlgorithm(
688 *args.find('Z')->second);
689 }
690
691 if (args.find('E') != args.end())
692 params.enforce_limits = true;
693 if (args.find('Q') != args.end()) {
694 params.nested_kcatalog_limit = String2Uint64(*args.find('Q')->second);
695 } else {
696 params.nested_kcatalog_limit = SyncParameters::kDefaultNestedKcatalogLimit;
697 }
698 if (args.find('R') != args.end()) {
699 params.root_kcatalog_limit = String2Uint64(*args.find('R')->second);
700 } else {
701 params.root_kcatalog_limit = SyncParameters::kDefaultRootKcatalogLimit;
702 }
703 if (args.find('U') != args.end()) {
704 params.file_mbyte_limit = String2Uint64(*args.find('U')->second);
705 } else {
706 params.file_mbyte_limit = SyncParameters::kDefaultFileMbyteLimit;
707 }
708
709 if (args.find('v') != args.end()) {
710 const sanitizer::IntegerSanitizer sanitizer;
711 if (!sanitizer.IsValid(*args.find('v')->second)) {
712 PrintError("Swissknife Sync: Invalid revision number");
713 return 1;
714 }
715 params.manual_revision = String2Uint64(*args.find('v')->second);
716 }
717
718 params.branched_catalog = args.find('B') != args.end();
719
720 if (args.find('q') != args.end()) {
721 params.max_concurrent_write_jobs = String2Uint64(*args.find('q')->second);
722 }
723
724 if (args.find('0') != args.end()) {
725 params.num_upload_tasks = String2Uint64(*args.find('0')->second);
726 }
727
728 if (args.find('T') != args.end()) {
729 params.ttl_seconds = String2Uint64(*args.find('T')->second);
730 }
731
732 if (args.find('g') != args.end()) {
733 params.ignore_special_files = true;
734 }
735
736 if (args.find('P') != args.end()) {
737 params.session_token_file = *args.find('P')->second;
738 }
739
740 if (args.find('H') != args.end()) {
741 params.key_file = *args.find('H')->second;
742 }
743
744 if (args.find('D') != args.end()) {
745 params.repo_tag.SetName(*args.find('D')->second);
746 }
747
748 if (args.find('J') != args.end()) {
749 params.repo_tag.SetDescription(*args.find('J')->second);
750 }
751
752 if (args.find('G') != args.end()) {
753 params.cache_dir = "/var/spool/cvmfs/" + params.repo_name + "/cache.server";
754 }
755
756 const bool upload_statsdb = (args.count('I') > 0);
757
758 if (!CheckParams(params))
759 return 2;
760 // This may fail, in which case a warning is printed and the process continues
761 ObtainDacReadSearchCapability();
762
763 perf::StatisticsTemplate publish_statistics("publish", this->statistics());
764
765 // Start spooler
766 upload::SpoolerDefinition spooler_definition(
767 params.spooler_definition, hash_algorithm, params.compression_alg,
768 params.generate_legacy_bulk_chunks, params.use_file_chunking,
769 params.min_file_chunk_size, params.avg_file_chunk_size,
770 params.max_file_chunk_size, params.session_token_file, params.key_file);
771 if (params.max_concurrent_write_jobs > 0) {
772 spooler_definition
773 .number_of_concurrent_uploads = params.max_concurrent_write_jobs;
774 }
775 spooler_definition.num_upload_tasks = params.num_upload_tasks;
776
777 const upload::SpoolerDefinition spooler_definition_catalogs(
778 spooler_definition.Dup2DefaultCompression());
779
780 params.spooler = upload::Spooler::Construct(spooler_definition,
781 &publish_statistics);
782 if (NULL == params.spooler)
783 return 3;
784 const UniquePtr<upload::Spooler> spooler_catalogs(upload::Spooler::Construct(
785 spooler_definition_catalogs, &publish_statistics));
786 if (!spooler_catalogs.IsValid())
787 return 3;
788
789 const bool follow_redirects = (args.count('L') > 0);
790 const string proxy = (args.count('@') > 0) ? *args.find('@')->second : "";
791 if (!InitDownloadManager(follow_redirects, proxy)) {
792 return 3;
793 }
794
795 if (!InitSignatureManager(params.public_keys)) {
796 return 3;
797 }
798
799 /*
800 * Note: If the upstream is of type gateway, due to the possibility of
801 * concurrent release managers, it's possible to have a different local and
802 * remote root hashes. We proceed by loading the remote manifest but we give
803 * an empty base hash.
804 */
805 UniquePtr<manifest::Manifest> manifest;
806 if (params.branched_catalog) {
807 // Throw-away manifest
808 manifest = new manifest::Manifest(shash::Any(), 0, "");
809 } else if (params.virtual_dir_actions
810 != catalog::VirtualCatalog::kActionNone) {
811 manifest = this->OpenLocalManifest(params.manifest_path);
812 params.base_hash = manifest->catalog_hash();
813 } else {
814 // TODO(jblomer): revert to params.base_hash if spooler driver type is not
815 // upload::SpoolerDefinition::Gateway
816 manifest = FetchRemoteManifest(params.stratum0, params.repo_name,
817 shash::Any());
818 }
819 if (!manifest.IsValid()) {
820 return 3;
821 }
822
823 StatisticsDatabase *stats_db = StatisticsDatabase::OpenStandardDB(
824 params.repo_name);
825
826 const std::string old_root_hash = manifest->catalog_hash().ToString(true);
827
828 catalog::WritableCatalogManager catalog_manager(
829 params.base_hash, params.stratum0, params.dir_temp,
830 spooler_catalogs.weak_ref(), download_manager(), params.enforce_limits,
831 params.nested_kcatalog_limit, params.root_kcatalog_limit,
832 params.file_mbyte_limit, statistics(), params.is_balanced,
833 params.max_weight, params.min_weight, params.cache_dir);
834 catalog_manager.Init();
835
836 publish::SyncMediator mediator(&catalog_manager, &params, publish_statistics);
837 LogCvmfs(kLogPublish, kLogStdout, "Swissknife Sync: Processing changes...");
838
839 // Should be before the synchronization starts to avoid race of GetTTL with
840 // other sqlite operations
841 if ((params.ttl_seconds > 0)
842 && ((params.ttl_seconds != catalog_manager.GetTTL())
843 || !catalog_manager.HasExplicitTTL())) {
844 LogCvmfs(kLogCvmfs, kLogStdout,
845 "Swissknife Sync: Setting repository TTL to %" PRIu64 "s",
846 params.ttl_seconds);
847 catalog_manager.SetTTL(params.ttl_seconds);
848 }
849
850 // Either real catalogs or virtual catalog
851 if (params.virtual_dir_actions == catalog::VirtualCatalog::kActionNone) {
852 publish::SyncUnion *sync;
853 if (params.union_fs_type == "overlayfs") {
854 sync = new publish::SyncUnionOverlayfs(
855 &mediator, params.dir_rdonly, params.dir_union, params.dir_scratch);
856 } else if (params.union_fs_type == "aufs") {
857 sync = new publish::SyncUnionAufs(&mediator, params.dir_rdonly,
858 params.dir_union, params.dir_scratch);
859 } else {
860 LogCvmfs(kLogCvmfs, kLogStderr,
861 "Swissknife Sync: unknown union file system: %s",
862 params.union_fs_type.c_str());
863 return 3;
864 }
865
866 if (!sync->Initialize()) {
867 LogCvmfs(kLogCvmfs, kLogStderr,
868 "Swissknife Sync: Initialization of the synchronisation "
869 "engine failed");
870 return 4;
871 }
872
873 sync->Traverse();
874 } else {
875 assert(!manifest->history().IsNull());
876 catalog::VirtualCatalog virtual_catalog(
877 manifest.weak_ref(), download_manager(), &catalog_manager, &params);
878 virtual_catalog.Generate(params.virtual_dir_actions);
879 }
880
881 if (!params.authz_file.empty()) {
882 LogCvmfs(kLogCvmfs, kLogDebug,
883 "Swissknife Sync: Adding contents of authz file %s to"
884 " root catalog.",
885 params.authz_file.c_str());
886 const int fd = open(params.authz_file.c_str(), O_RDONLY);
887 if (fd == -1) {
888 LogCvmfs(kLogCvmfs, kLogStderr,
889 "Swissknife Sync: Unable to open authz file (%s)"
890 "from the publication process: %s",
891 params.authz_file.c_str(), strerror(errno));
892 return 7;
893 }
894
895 std::string new_authz;
896 const bool read_successful = SafeReadToString(fd, &new_authz);
897 close(fd);
898
899 if (!read_successful) {
900 LogCvmfs(kLogCvmfs, kLogStderr,
901 "Swissknife Sync: Failed to read authz file (%s): %s",
902 params.authz_file.c_str(), strerror(errno));
903 return 8;
904 }
905
906 catalog_manager.SetVOMSAuthz(new_authz);
907 }
908
909 if (!mediator.Commit(manifest.weak_ref())) {
910 PrintError("Swissknife Sync: Something went wrong during sync");
911 if (!params.dry_run) {
912 stats_db->StorePublishStatistics(this->statistics(), start_time, false);
913 if (upload_statsdb) {
914 stats_db->UploadStatistics(params.spooler);
915 }
916 }
917 return 5;
918 }
919
920 perf::Counter *revision_counter = statistics()->Register(
921 "publish.revision", "Published revision number");
922 revision_counter->Set(
923 static_cast<int64_t>(catalog_manager.GetRootCatalog()->revision()));
924
925 // finalize the spooler
926 LogCvmfs(kLogCvmfs, kLogStdout,
927 "Swissknife Sync: Wait for all uploads to finish");
928 params.spooler->WaitForUpload();
929 spooler_catalogs->WaitForUpload();
930 params.spooler->FinalizeSession(false);
931
932 LogCvmfs(kLogCvmfs, kLogStdout,
933 "Swissknife Sync: Exporting repository manifest");
934
935 // We call FinalizeSession(true) this time, to also trigger the commit
936 // operation on the gateway machine (if the upstream is of type "gw").
937
938 // Get the path of the new root catalog
939 const std::string new_root_hash = manifest->catalog_hash().ToString(true);
940
941 if (!spooler_catalogs->FinalizeSession(true, old_root_hash, new_root_hash,
942 params.repo_tag)) {
943 PrintError("Swissknife Sync: Failed to commit transaction.");
944 if (!params.dry_run) {
945 stats_db->StorePublishStatistics(this->statistics(), start_time, false);
946 if (upload_statsdb) {
947 stats_db->UploadStatistics(params.spooler);
948 }
949 }
950 return 9;
951 }
952
953 if (!params.dry_run) {
954 stats_db->StorePublishStatistics(this->statistics(), start_time, true);
955 if (upload_statsdb) {
956 stats_db->UploadStatistics(params.spooler);
957 }
958 }
959
960 delete params.spooler;
961
962 if (!manifest->Export(params.manifest_path)) {
963 PrintError("Swissknife Sync: Failed to create new repository");
964 return 6;
965 }
966
967 return 0;
968 }
969