GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/sync_mediator.cc
Date: 2026-07-19 02:35:15
Exec Total Coverage
Lines: 1 631 0.2%
Branches: 0 956 0.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System.
3 */
4
5 #include "sync_mediator.h"
6
7 #include <fcntl.h>
8 #include <inttypes.h>
9 #include <unistd.h>
10
11 #include <cassert>
12 #include <cstdio>
13 #include <cstdlib>
14
15 #include "catalog_virtual.h"
16 #include "compression/compression.h"
17 #include "crypto/hash.h"
18 #include "directory_entry.h"
19 #include "json_document.h"
20 #include "publish/repository.h"
21 #include "sync_union.h"
22 #include "upload.h"
23 #include "util/exception.h"
24 #include "util/fs_traversal.h"
25 #include "util/posix.h"
26 #include "util/string.h"
27
28 using namespace std; // NOLINT
29
30 namespace publish {
31
32 36 AbstractSyncMediator::~AbstractSyncMediator() { }
33
34 SyncMediator::SyncMediator(catalog::WritableCatalogManager *catalog_manager,
35 const SyncParameters *params,
36 perf::StatisticsTemplate statistics)
37 : catalog_manager_(catalog_manager)
38 , union_engine_(NULL)
39 , handle_hardlinks_(false)
40 , recursive_fast_delete_(false)
41 , params_(params)
42 , reporter_(new SyncDiffReporter(params_->print_changeset
43 ? SyncDiffReporter::kPrintChanges
44 : SyncDiffReporter::kPrintDots)) {
45 const int retval = pthread_mutex_init(&lock_file_queue_, NULL);
46 assert(retval == 0);
47
48 params->spooler->RegisterListener(&SyncMediator::PublishFilesCallback, this);
49
50 counters_ = new perf::FsCounters(statistics);
51 }
52
53 SyncMediator::~SyncMediator() { pthread_mutex_destroy(&lock_file_queue_); }
54
55
56 void SyncMediator::RegisterUnionEngine(SyncUnion *engine) {
57 union_engine_ = engine;
58 handle_hardlinks_ = engine->SupportsHardlinks();
59 }
60
61
62 /**
63 * The entry /.cvmfs or entries in /.cvmfs/ must not be added, removed or
64 * modified manually. The directory /.cvmfs is generated by the VirtualCatalog
65 * class if requested.
66 */
67 void SyncMediator::EnsureAllowed(SharedPtr<SyncItem> entry) {
68 const bool ignore_case_setting = false;
69 const string relative_path = entry->GetRelativePath();
70 if ((relative_path == string(catalog::VirtualCatalog::kVirtualPath))
71 || (HasPrefix(relative_path,
72 string(catalog::VirtualCatalog::kVirtualPath) + "/",
73 ignore_case_setting))) {
74 PANIC(kLogStderr, "[ERROR] invalid attempt to modify %s",
75 relative_path.c_str());
76 }
77 }
78
79
80 /**
81 * Add an entry to the repository.
82 * Added directories will be traversed in order to add the complete subtree.
83 */
84 void SyncMediator::Add(SharedPtr<SyncItem> entry) {
85 EnsureAllowed(entry);
86
87 if (entry->IsDirectory()) {
88 AddDirectoryRecursively(entry);
89 return;
90 }
91
92 if (entry->IsBundleSpec()) {
93 PrintWarning(".cvmfsbundle file encountered. "
94 "Bundles is currently an experimental feature.");
95
96 if (!entry->IsRegularFile() && !entry->IsSymlink()) {
97 PANIC(kLogStderr,
98 "Error: bundle specification must be a regular file or a symlink");
99 }
100 if (entry->HasHardlinks()) {
101 PANIC(kLogStderr, "Error: bundle specification must not be a hard link");
102 }
103
104 InsertBundleSpec(entry);
105 AddFile(entry);
106 return;
107 }
108
109 if (entry->IsRegularFile() || entry->IsSymlink()) {
110 // A file is a hard link if the link count is greater than 1
111 if (entry->HasHardlinks() && handle_hardlinks_)
112 InsertHardlink(entry);
113 else
114 AddFile(entry);
115 return;
116 } else if (entry->IsGraftMarker()) {
117 LogCvmfs(kLogPublish, kLogDebug, "Ignoring graft marker file.");
118 return; // Ignore markers.
119 }
120
121 // In OverlayFS whiteouts can be represented as character devices with major
122 // and minor numbers equal to 0. Special files will be ignored except if they
123 // are whiteout files.
124 if (entry->IsSpecialFile() && !entry->IsWhiteout()) {
125 if (params_->ignore_special_files) {
126 PrintWarning("'" + entry->GetRelativePath()
127 + "' "
128 "is a special file, ignoring.");
129 } else {
130 if (entry->HasHardlinks() && handle_hardlinks_)
131 InsertHardlink(entry);
132 else
133 AddFile(entry);
134 }
135 return;
136 }
137
138 PrintWarning("'" + entry->GetRelativePath()
139 + "' cannot be added. Unrecognized file type.");
140 }
141
142
143 /**
144 * Touch an entry in the repository.
145 */
146 void SyncMediator::Touch(SharedPtr<SyncItem> entry) {
147 EnsureAllowed(entry);
148
149 if (entry->IsGraftMarker()) {
150 return;
151 }
152 if (entry->IsDirectory()) {
153 TouchDirectory(entry);
154 perf::Inc(counters_->n_directories_changed);
155 return;
156 }
157
158 if (entry->IsRegularFile() || entry->IsSymlink() || entry->IsSpecialFile()) {
159 Replace(entry); // This way, hardlink processing is correct
160 // Replace calls Remove; cancel Remove's actions:
161 perf::Xadd(counters_->sz_removed_bytes, -entry->GetRdOnlySize());
162
163 // Count only the difference between the old and new file
164 // Symlinks do not count into added or removed bytes
165 int64_t dif = 0;
166
167 // Need to handle 4 cases (symlink->symlink, symlink->regular,
168 // regular->symlink, regular->regular)
169 if (entry->WasSymlink()) {
170 // Replace calls Remove; cancel Remove's actions:
171 perf::Dec(counters_->n_symlinks_removed);
172
173 if (entry->IsSymlink()) {
174 perf::Inc(counters_->n_symlinks_changed);
175 } else {
176 perf::Inc(counters_->n_symlinks_removed);
177 perf::Inc(counters_->n_files_added);
178 dif += entry->GetScratchSize();
179 }
180 } else {
181 // Replace calls Remove; cancel Remove's actions:
182 perf::Dec(counters_->n_files_removed);
183 dif -= entry->GetRdOnlySize();
184 if (entry->IsSymlink()) {
185 perf::Inc(counters_->n_files_removed);
186 perf::Inc(counters_->n_symlinks_added);
187 } else {
188 perf::Inc(counters_->n_files_changed);
189 dif += entry->GetScratchSize();
190 }
191 }
192
193 if (dif > 0) { // added bytes
194 perf::Xadd(counters_->sz_added_bytes, dif);
195 } else { // removed bytes
196 perf::Xadd(counters_->sz_removed_bytes, -dif);
197 }
198 return;
199 }
200
201 PrintWarning("'" + entry->GetRelativePath()
202 + "' cannot be touched. Unrecognized file type.");
203 }
204
205
206 /**
207 * Remove an entry from the repository. Directories will be recursively removed.
208 */
209 void SyncMediator::Remove(SharedPtr<SyncItem> entry, bool fast_delete) {
210 EnsureAllowed(entry);
211
212 if (entry->WasDirectory()) {
213 RemoveDirectoryRecursively(entry, fast_delete);
214 return;
215 }
216
217 if (entry->WasBundleSpec()) {
218 if (!params_->dry_run)
219 catalog_manager_->UpdateBundleTrigger(GetBundleTriggerPath(entry), false);
220 RemoveFile(entry);
221 return;
222 }
223
224 if (entry->WasRegularFile() || entry->WasSymlink()
225 || entry->WasSpecialFile()) {
226 RemoveFile(entry);
227 return;
228 }
229
230 PrintWarning("'" + entry->GetRelativePath()
231 + "' cannot be deleted. Unrecognized file type.");
232 }
233
234
235 /**
236 * Remove the old entry and add the new one.
237 */
238 void SyncMediator::Replace(SharedPtr<SyncItem> entry) {
239 // EnsureAllowed(entry); <-- Done by Remove() and Add()
240 Remove(entry);
241 Add(entry);
242 }
243
244 bool SyncMediator::Clone(const std::string from, const std::string to,
245 bool fail_if_source_missing) {
246 return catalog_manager_->Clone(from, to, fail_if_source_missing);
247 }
248
249 void SyncMediator::EnterDirectory(SharedPtr<SyncItem> entry) {
250 if (!handle_hardlinks_) {
251 return;
252 }
253
254 const HardlinkGroupMap new_map;
255 hardlink_stack_.push(new_map);
256 }
257
258
259 void SyncMediator::LeaveDirectory(SharedPtr<SyncItem> entry) {
260 if (!handle_hardlinks_) {
261 return;
262 }
263
264 CompleteHardlinks(entry);
265 AddLocalHardlinkGroups(GetHardlinkMap());
266 hardlink_stack_.pop();
267 }
268
269
270 /**
271 * Do any pending processing and commit all changes to the catalogs.
272 * To be called after change set traversal is finished.
273 */
274 bool SyncMediator::Commit(manifest::Manifest *manifest) {
275 reporter_->CommitReport();
276
277 if (!params_->dry_run) {
278 LogCvmfs(kLogPublish, kLogStdout,
279 "Waiting for upload of files before committing...");
280 params_->spooler->WaitForUpload();
281 }
282
283 if (!hardlink_queue_.empty()) {
284 assert(handle_hardlinks_);
285
286 LogCvmfs(kLogPublish, kLogStdout, "Processing hardlinks...");
287 params_->spooler->UnregisterListeners();
288 params_->spooler->RegisterListener(&SyncMediator::PublishHardlinksCallback,
289 this);
290
291 // TODO(rmeusel): Revise that for Thread Safety!
292 // This loop will spool hardlinks into the spooler, which will then
293 // process them.
294 // On completion of every hardlink the spooler will asynchronously
295 // emit callbacks (SyncMediator::PublishHardlinksCallback) which
296 // might happen while this for-loop goes through the hardlink_queue_
297 //
298 // For the moment this seems not to be a problem, but it's an accident
299 // just waiting to happen.
300 //
301 // Note: Just wrapping this loop in a mutex might produce a dead lock
302 // since the spooler does not fill it's processing queue to an
303 // unlimited size. Meaning that it might be flooded with hard-
304 // links and waiting for the queue to be processed while proces-
305 // sing is stalled because the callback is waiting for this
306 // mutex.
307 for (HardlinkGroupList::const_iterator i = hardlink_queue_.begin(),
308 iEnd = hardlink_queue_.end();
309 i != iEnd;
310 ++i) {
311 LogCvmfs(kLogPublish, kLogVerboseMsg, "Spooling hardlink group %s",
312 i->master->GetUnionPath().c_str());
313 IngestionSource *source = new FileIngestionSource(
314 i->master->GetUnionPath());
315 params_->spooler->Process(source);
316 }
317
318 params_->spooler->WaitForUpload();
319
320 for (HardlinkGroupList::const_iterator i = hardlink_queue_.begin(),
321 iEnd = hardlink_queue_.end();
322 i != iEnd;
323 ++i) {
324 LogCvmfs(kLogPublish, kLogVerboseMsg, "Processing hardlink group %s",
325 i->master->GetUnionPath().c_str());
326 AddHardlinkGroup(*i);
327 }
328 }
329
330 if (!bundle_specs_.empty()) {
331 LogCvmfs(kLogPublish, kLogStdout, "Processing hardlinks...");
332 AddBundleSpecs();
333 }
334
335 if (union_engine_)
336 union_engine_->PostUpload();
337
338 // PostUpload may have spooled additional files (the tarball engine
339 // materializes empty files for hardlinks whose target is missing from the
340 // archive). Wait for those uploads so their catalog entries are added by
341 // the file callback before the listeners are unregistered below.
342 if (!params_->dry_run)
343 params_->spooler->WaitForUpload();
344
345 params_->spooler->UnregisterListeners();
346
347 if (params_->dry_run) {
348 manifest = NULL;
349 return true;
350 }
351
352 LogCvmfs(kLogPublish, kLogStdout, "Committing file catalogs...");
353 if (params_->spooler->GetNumberOfErrors() > 0) {
354 LogCvmfs(kLogPublish, kLogStderr, "failed to commit files");
355 return false;
356 }
357
358 if (catalog_manager_->IsBalanceable()
359 || (params_->virtual_dir_actions
360 != catalog::VirtualCatalog::kActionNone)) {
361 if (catalog_manager_->IsBalanceable())
362 catalog_manager_->Balance();
363 // Commit empty string to ensure that the "content" of the auto catalog
364 // markers is present in the repository.
365 const string empty_file = CreateTempPath(params_->dir_temp + "/empty",
366 0600);
367 IngestionSource *source = new FileIngestionSource(empty_file);
368 params_->spooler->Process(source);
369 params_->spooler->WaitForUpload();
370 unlink(empty_file.c_str());
371 if (params_->spooler->GetNumberOfErrors() > 0) {
372 LogCvmfs(kLogPublish, kLogStderr, "failed to commit auto catalog marker");
373 return false;
374 }
375 }
376 catalog_manager_->PrecalculateListings();
377 return catalog_manager_->Commit(params_->stop_for_catalog_tweaks,
378 params_->manual_revision, manifest);
379 }
380
381 std::string SyncMediator::GetBundleTriggerPath(
382 SharedPtr<SyncItem> bundle_spec_entry) const
383 {
384 static const size_t nStrip = strlen(".cvmfsbundle-");
385 const std::string main_file_name = bundle_spec_entry->filename().substr(nStrip);
386 if (main_file_name.empty()) {
387 PANIC(kLogStderr, "invalid empty bundle specification: %s",
388 bundle_spec_entry->GetUnionPath().c_str());
389 }
390 // relative_parent_path() is empty for the repo root and otherwise lacks a
391 // leading slash (e.g. "root/lib/ROOT/__pycache__"). The catalog lookup in
392 // WritableCatalogManager::FindCatalog needs an absolute path, so prepend
393 // "/" — but skip it when relative_parent_path() is empty, in which case
394 // the existing literal "/" already produces a valid "/<basename>".
395 const std::string &parent = bundle_spec_entry->relative_parent_path();
396 return (parent.empty() ? "" : "/" + parent) + "/" + main_file_name;
397 }
398
399 void SyncMediator::InsertBundleSpec(SharedPtr<SyncItem> entry) {
400 assert(entry->IsBundleSpec());
401
402 // When we leave the directory, we'll check all the bundle specs and set
403 // the corresponding flags on the main file.
404 bundle_specs_.push_back(GetBundleTriggerPath(entry));
405 }
406
407 void SyncMediator::AddBundleSpecs() {
408 if (params_->dry_run)
409 return;
410
411 for (BundleSpecs::const_iterator itr = bundle_specs_.begin();
412 itr != bundle_specs_.end(); ++itr)
413 {
414 catalog_manager_->UpdateBundleTrigger(*itr, true);
415 }
416 }
417
418 void SyncMediator::InsertHardlink(SharedPtr<SyncItem> entry) {
419 assert(handle_hardlinks_);
420
421 const uint64_t inode = entry->GetUnionInode();
422 LogCvmfs(kLogPublish, kLogVerboseMsg, "found hardlink %" PRIu64 " at %s",
423 inode, entry->GetUnionPath().c_str());
424
425 // Find the hard link group in the lists
426 const HardlinkGroupMap::iterator hardlink_group = GetHardlinkMap().find(
427 inode);
428
429 if (hardlink_group == GetHardlinkMap().end()) {
430 // Create a new hardlink group
431 GetHardlinkMap().insert(
432 HardlinkGroupMap::value_type(inode, HardlinkGroup(entry)));
433 } else {
434 // Append the file to the appropriate hardlink group
435 hardlink_group->second.AddHardlink(entry);
436 }
437
438 // publish statistics counting for new file
439 if (entry->IsNew()) {
440 perf::Inc(counters_->n_files_added);
441 perf::Xadd(counters_->sz_added_bytes, entry->GetScratchSize());
442 }
443 }
444
445
446 void SyncMediator::InsertLegacyHardlink(SharedPtr<SyncItem> entry) {
447 // Check if found file has hardlinks (nlink > 1)
448 // As we are looking through all files in one directory here, there might be
449 // completely untouched hardlink groups, which we can safely skip.
450 // Finally we have to see if the hardlink is already part of this group
451
452 assert(handle_hardlinks_);
453
454 if (entry->GetUnionLinkcount() < 2)
455 return;
456
457 const uint64_t inode = entry->GetUnionInode();
458 HardlinkGroupMap::iterator hl_group;
459 hl_group = GetHardlinkMap().find(inode);
460
461 if (hl_group != GetHardlinkMap().end()) { // touched hardlinks in this group?
462 bool found = false;
463
464 // search for the entry in this group
465 for (SyncItemList::const_iterator i = hl_group->second.hardlinks.begin(),
466 iEnd = hl_group->second.hardlinks.end();
467 i != iEnd;
468 ++i) {
469 if (*(i->second) == *entry) {
470 found = true;
471 break;
472 }
473 }
474
475 if (!found) {
476 // Hardlink already in the group?
477 // If one element of a hardlink group is edited, all elements must be
478 // replaced. Here, we remove an untouched hardlink and add it to its
479 // hardlink group for re-adding later
480 LogCvmfs(kLogPublish, kLogVerboseMsg, "Picked up legacy hardlink %s",
481 entry->GetUnionPath().c_str());
482 Remove(entry);
483 hl_group->second.AddHardlink(entry);
484 }
485 }
486 }
487
488
489 /**
490 * Create a recursion engine which DOES NOT recurse into directories.
491 * It basically goes through the current directory (in the union volume) and
492 * searches for legacy hardlinks which has to be connected to the new
493 * or edited ones.
494 */
495 void SyncMediator::CompleteHardlinks(SharedPtr<SyncItem> entry) {
496 assert(handle_hardlinks_);
497
498 // If no hardlink in this directory was changed, we can skip this
499 if (GetHardlinkMap().empty())
500 return;
501
502 LogCvmfs(kLogPublish, kLogVerboseMsg, "Post-processing hard links in %s",
503 entry->GetUnionPath().c_str());
504
505 // Look for legacy hardlinks
506 FileSystemTraversal<SyncMediator> traversal(this, union_engine_->union_path(),
507 false);
508 traversal.fn_new_file = &SyncMediator::LegacyRegularHardlinkCallback;
509 traversal.fn_new_symlink = &SyncMediator::LegacySymlinkHardlinkCallback;
510 traversal.fn_new_character_dev = &SyncMediator::
511 LegacyCharacterDeviceHardlinkCallback;
512 traversal.fn_new_block_dev = &SyncMediator::LegacyBlockDeviceHardlinkCallback;
513 traversal.fn_new_fifo = &SyncMediator::LegacyFifoHardlinkCallback;
514 traversal.fn_new_socket = &SyncMediator::LegacySocketHardlinkCallback;
515 traversal.Recurse(entry->GetUnionPath());
516 }
517
518
519 void SyncMediator::LegacyRegularHardlinkCallback(const string &parent_dir,
520 const string &file_name) {
521 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, file_name,
522 kItemFile);
523 InsertLegacyHardlink(entry);
524 }
525
526
527 void SyncMediator::LegacySymlinkHardlinkCallback(const string &parent_dir,
528 const string &file_name) {
529 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, file_name,
530 kItemSymlink);
531 InsertLegacyHardlink(entry);
532 }
533
534 void SyncMediator::LegacyCharacterDeviceHardlinkCallback(
535 const string &parent_dir, const string &file_name) {
536 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, file_name,
537 kItemCharacterDevice);
538 InsertLegacyHardlink(entry);
539 }
540
541 void SyncMediator::LegacyBlockDeviceHardlinkCallback(const string &parent_dir,
542 const string &file_name) {
543 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, file_name,
544 kItemBlockDevice);
545 InsertLegacyHardlink(entry);
546 }
547
548 void SyncMediator::LegacyFifoHardlinkCallback(const string &parent_dir,
549 const string &file_name) {
550 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, file_name,
551 kItemFifo);
552 InsertLegacyHardlink(entry);
553 }
554
555 void SyncMediator::LegacySocketHardlinkCallback(const string &parent_dir,
556 const string &file_name) {
557 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, file_name,
558 kItemSocket);
559 InsertLegacyHardlink(entry);
560 }
561
562
563 void SyncMediator::AddDirectoryRecursively(SharedPtr<SyncItem> entry) {
564 AddDirectory(entry);
565
566 // Create a recursion engine, which recursively adds all entries in a newly
567 // created directory
568 FileSystemTraversal<SyncMediator> traversal(
569 this, union_engine_->scratch_path(), true);
570 traversal.fn_enter_dir = &SyncMediator::EnterAddedDirectoryCallback;
571 traversal.fn_leave_dir = &SyncMediator::LeaveAddedDirectoryCallback;
572 traversal.fn_new_file = &SyncMediator::AddFileCallback;
573 traversal.fn_new_symlink = &SyncMediator::AddSymlinkCallback;
574 traversal.fn_new_dir_prefix = &SyncMediator::AddDirectoryCallback;
575 traversal.fn_ignore_file = &SyncMediator::IgnoreFileCallback;
576 traversal.fn_new_character_dev = &SyncMediator::AddCharacterDeviceCallback;
577 traversal.fn_new_block_dev = &SyncMediator::AddBlockDeviceCallback;
578 traversal.fn_new_fifo = &SyncMediator::AddFifoCallback;
579 traversal.fn_new_socket = &SyncMediator::AddSocketCallback;
580 traversal.Recurse(entry->GetScratchPath());
581 }
582
583
584 bool SyncMediator::AddDirectoryCallback(const std::string &parent_dir,
585 const std::string &dir_name) {
586 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, dir_name,
587 kItemDir);
588 AddDirectory(entry);
589 return true; // The recursion engine should recurse deeper here
590 }
591
592
593 void SyncMediator::AddFileCallback(const std::string &parent_dir,
594 const std::string &file_name) {
595 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, file_name,
596 kItemFile);
597 Add(entry);
598 }
599
600
601 void SyncMediator::AddCharacterDeviceCallback(const std::string &parent_dir,
602 const std::string &file_name) {
603 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, file_name,
604 kItemCharacterDevice);
605 Add(entry);
606 }
607
608 void SyncMediator::AddBlockDeviceCallback(const std::string &parent_dir,
609 const std::string &file_name) {
610 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, file_name,
611 kItemBlockDevice);
612 Add(entry);
613 }
614
615 void SyncMediator::AddFifoCallback(const std::string &parent_dir,
616 const std::string &file_name) {
617 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, file_name,
618 kItemFifo);
619 Add(entry);
620 }
621
622 void SyncMediator::AddSocketCallback(const std::string &parent_dir,
623 const std::string &file_name) {
624 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, file_name,
625 kItemSocket);
626 Add(entry);
627 }
628
629 void SyncMediator::AddSymlinkCallback(const std::string &parent_dir,
630 const std::string &link_name) {
631 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, link_name,
632 kItemSymlink);
633 Add(entry);
634 }
635
636
637 void SyncMediator::EnterAddedDirectoryCallback(const std::string &parent_dir,
638 const std::string &dir_name) {
639 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, dir_name,
640 kItemDir);
641 EnterDirectory(entry);
642 }
643
644
645 void SyncMediator::LeaveAddedDirectoryCallback(const std::string &parent_dir,
646 const std::string &dir_name) {
647 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, dir_name,
648 kItemDir);
649 LeaveDirectory(entry);
650 }
651
652
653 void SyncMediator::RemoveDirectoryRecursively(SharedPtr<SyncItem> entry,
654 bool fast_delete) {
655 const std::string directory_path = entry->GetRelativePath();
656
657 // Fast delete: skip filesystem traversal for nested catalog directories.
658 // Instead of recursively walking the filesystem and removing each entry,
659 // we just remove the nested catalog reference from the parent catalog
660 // (with merge=false so entries are not copied to the parent) and then
661 // remove the mountpoint directory entry.
662 if (fast_delete && catalog_manager_->IsTransitionPoint(directory_path)) {
663 // Get the nested catalog's counters before removal so we can update
664 // publish statistics with the total number of removed entries. Because the
665 // filesystem is not walked, these aggregate counters (self + subtree) are
666 // the only source for the removal statistics. self.directories already
667 // accounts for the nested catalog root, which is the same filesystem
668 // directory as the mountpoint removed from the parent below, so it must not
669 // be counted again (the normal traversal path also counts it exactly once).
670 // Likewise, every descendant nested catalog is represented twice in the
671 // aggregate counters (mountpoint + root), so subtract the nested catalog
672 // count from the aggregate directory count below.
673 std::string subcatalog_path;
674 shash::Any hash;
675 // LookupCounters expects an absolute catalog path (leading slash); the sync
676 // item's relative path does not carry one, so prepend it here. Without the
677 // slash the prefix match falls back to the root catalog and the removal
678 // statistics would account for the whole repository instead of the subtree.
679 const std::string absolute_path = "/" + directory_path;
680 PathString ps_path;
681 ps_path.Assign(absolute_path.data(), absolute_path.length());
682 const catalog::Counters counters =
683 catalog_manager_->LookupCounters(ps_path, &subcatalog_path, &hash);
684 // On failure to load the subtree LookupCounters returns zeroed counters and
685 // a null hash. Removal proceeds regardless, but the statistics would
686 // silently under-count, so warn.
687 if (hash.IsNull()) {
688 LogCvmfs(kLogPublish, kLogStderr | kLogSyslogWarn,
689 "Warning: could not read counters of nested catalog at '%s'; "
690 "removal statistics may be incomplete",
691 directory_path.c_str());
692 }
693 {
694 perf::Xadd(counters_->n_files_removed,
695 static_cast<int64_t>(counters.self.regular_files
696 + counters.subtree.regular_files));
697 const uint64_t n_nested_catalogs = counters.self.nested_catalogs
698 + counters.subtree.nested_catalogs;
699 const uint64_t n_directories = counters.self.directories
700 + counters.subtree.directories;
701 perf::Xadd(counters_->n_directories_removed,
702 static_cast<int64_t>(n_directories - n_nested_catalogs));
703 perf::Xadd(counters_->n_symlinks_removed,
704 static_cast<int64_t>(counters.self.symlinks
705 + counters.subtree.symlinks));
706 perf::Xadd(counters_->sz_removed_bytes,
707 static_cast<int64_t>(counters.self.file_size
708 + counters.subtree.file_size));
709 }
710
711 // Remove nested catalog (merge=false: just remove the reference and
712 // adjust parent subtree counters, don't copy entries into parent)
713 const std::string notice = "Nested catalog at " + entry->GetUnionPath();
714 reporter_->OnRemove(notice, catalog::DirectoryEntry());
715 if (!params_->dry_run) {
716 catalog_manager_->RemoveNestedCatalog(directory_path, false);
717 }
718
719 // Remove the mountpoint directory entry from the parent catalog
720 reporter_->OnRemove(entry->GetUnionPath(), catalog::DirectoryEntry());
721 if (!params_->dry_run) {
722 catalog_manager_->RemoveDirectory(directory_path);
723 }
724
725 return;
726 }
727
728 // Normal path: delete a directory AFTER it was emptied here,
729 // because it would start up another recursion.
730 // Propagate fast_delete so that any nested catalog transition points
731 // encountered during the traversal are still fast-deleted.
732 const bool prev_fast_delete = recursive_fast_delete_;
733 if (fast_delete) recursive_fast_delete_ = true;
734
735 const bool recurse = false;
736 FileSystemTraversal<SyncMediator> traversal(
737 this, union_engine_->rdonly_path(), recurse);
738 traversal.fn_new_file = &SyncMediator::RemoveFileCallback;
739 traversal.fn_new_dir_postfix = &SyncMediator::RemoveDirectoryCallback;
740 traversal.fn_new_symlink = &SyncMediator::RemoveSymlinkCallback;
741 traversal.fn_new_character_dev = &SyncMediator::RemoveCharacterDeviceCallback;
742 traversal.fn_new_block_dev = &SyncMediator::RemoveBlockDeviceCallback;
743 traversal.fn_new_fifo = &SyncMediator::RemoveFifoCallback;
744 traversal.fn_new_socket = &SyncMediator::RemoveSocketCallback;
745 traversal.Recurse(entry->GetRdOnlyPath());
746
747 recursive_fast_delete_ = prev_fast_delete;
748
749 // The given directory was emptied recursively and can now itself be deleted
750 RemoveDirectory(entry);
751 }
752
753
754 void SyncMediator::RemoveFileCallback(const std::string &parent_dir,
755 const std::string &file_name) {
756 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, file_name,
757 kItemFile);
758 Remove(entry);
759 }
760
761
762 void SyncMediator::RemoveSymlinkCallback(const std::string &parent_dir,
763 const std::string &link_name) {
764 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, link_name,
765 kItemSymlink);
766 Remove(entry);
767 }
768
769 void SyncMediator::RemoveCharacterDeviceCallback(const std::string &parent_dir,
770 const std::string &link_name) {
771 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, link_name,
772 kItemCharacterDevice);
773 Remove(entry);
774 }
775
776 void SyncMediator::RemoveBlockDeviceCallback(const std::string &parent_dir,
777 const std::string &link_name) {
778 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, link_name,
779 kItemBlockDevice);
780 Remove(entry);
781 }
782
783 void SyncMediator::RemoveFifoCallback(const std::string &parent_dir,
784 const std::string &link_name) {
785 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, link_name,
786 kItemFifo);
787 Remove(entry);
788 }
789
790 void SyncMediator::RemoveSocketCallback(const std::string &parent_dir,
791 const std::string &link_name) {
792 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, link_name,
793 kItemSocket);
794 Remove(entry);
795 }
796
797 void SyncMediator::RemoveDirectoryCallback(const std::string &parent_dir,
798 const std::string &dir_name) {
799 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, dir_name,
800 kItemDir);
801 RemoveDirectoryRecursively(entry, recursive_fast_delete_);
802 }
803
804
805 bool SyncMediator::IgnoreFileCallback(const std::string &parent_dir,
806 const std::string &file_name) {
807 if (union_engine_->IgnoreFilePredicate(parent_dir, file_name)) {
808 return true;
809 }
810
811 const SharedPtr<SyncItem> entry = CreateSyncItem(parent_dir, file_name,
812 kItemUnknown);
813 return entry->IsWhiteout();
814 }
815
816 SharedPtr<SyncItem> SyncMediator::CreateSyncItem(
817 const std::string &relative_parent_path, const std::string &filename,
818 const SyncItemType entry_type) const {
819 return union_engine_->CreateSyncItem(relative_parent_path, filename,
820 entry_type);
821 }
822
823 void SyncMediator::PublishFilesCallback(const upload::SpoolerResult &result) {
824 LogCvmfs(kLogPublish, kLogVerboseMsg,
825 "Spooler callback for %s, digest %s, produced %lu chunks, retval %d",
826 result.local_path.c_str(), result.content_hash.ToString().c_str(),
827 result.file_chunks.size(), result.return_code);
828 if (result.return_code != 0) {
829 PANIC(kLogStderr, "Spool failure for %s (%d)", result.local_path.c_str(),
830 result.return_code);
831 }
832
833 SyncItemList::iterator itr;
834 {
835 const MutexLockGuard guard(lock_file_queue_);
836 itr = file_queue_.find(result.local_path);
837 }
838
839 assert(itr != file_queue_.end());
840
841 SyncItem &item = *itr->second;
842 item.SetContentHash(result.content_hash);
843 item.SetCompressionAlgorithm(result.compression_alg);
844
845 XattrList *xattrs = &default_xattrs_;
846 if (params_->include_xattrs) {
847 xattrs = XattrList::CreateFromFile(result.local_path);
848 assert(xattrs != NULL);
849 }
850
851 if (result.IsChunked()) {
852 catalog_manager_->AddChunkedFile(
853 item.CreateBasicCatalogDirent(params_->enable_mtime_ns),
854 *xattrs,
855 item.relative_parent_path(),
856 result.file_chunks);
857 } else {
858 catalog_manager_->AddFile(
859 item.CreateBasicCatalogDirent(params_->enable_mtime_ns),
860 *xattrs,
861 item.relative_parent_path());
862 }
863
864 if (xattrs != &default_xattrs_)
865 free(xattrs);
866 }
867
868
869 void SyncMediator::PublishHardlinksCallback(
870 const upload::SpoolerResult &result) {
871 LogCvmfs(kLogPublish, kLogVerboseMsg,
872 "Spooler callback for hardlink %s, digest %s, retval %d",
873 result.local_path.c_str(), result.content_hash.ToString().c_str(),
874 result.return_code);
875 if (result.return_code != 0) {
876 PANIC(kLogStderr, "Spool failure for %s (%d)", result.local_path.c_str(),
877 result.return_code);
878 }
879
880 bool found = false;
881 for (unsigned i = 0; i < hardlink_queue_.size(); ++i) {
882 if (hardlink_queue_[i].master->GetUnionPath() == result.local_path) {
883 found = true;
884 hardlink_queue_[i].master->SetContentHash(result.content_hash);
885 SyncItemList::iterator j, jend;
886 for (j = hardlink_queue_[i].hardlinks.begin(),
887 jend = hardlink_queue_[i].hardlinks.end();
888 j != jend;
889 ++j) {
890 j->second->SetContentHash(result.content_hash);
891 j->second->SetCompressionAlgorithm(result.compression_alg);
892 }
893 if (result.IsChunked())
894 hardlink_queue_[i].file_chunks = result.file_chunks;
895
896 break;
897 }
898 }
899
900 assert(found);
901 }
902
903
904 void SyncMediator::CreateNestedCatalog(SharedPtr<SyncItem> directory) {
905 const std::string notice = "Nested catalog at " + directory->GetUnionPath();
906 reporter_->OnAdd(notice, catalog::DirectoryEntry());
907
908 if (!params_->dry_run) {
909 catalog_manager_->CreateNestedCatalog(directory->GetRelativePath());
910 }
911 }
912
913
914 void SyncMediator::RemoveNestedCatalog(SharedPtr<SyncItem> directory) {
915 const std::string notice = "Nested catalog at " + directory->GetUnionPath();
916 reporter_->OnRemove(notice, catalog::DirectoryEntry());
917
918 if (!params_->dry_run) {
919 catalog_manager_->RemoveNestedCatalog(directory->GetRelativePath());
920 }
921 }
922
923 void SyncDiffReporter::OnInit(const history::History::Tag & /*from_tag*/,
924 const history::History::Tag & /*to_tag*/) { }
925
926 void SyncDiffReporter::OnStats(const catalog::DeltaCounters & /*delta*/) { }
927
928 void SyncDiffReporter::OnAdd(const std::string &path,
929 const catalog::DirectoryEntry & /*entry*/) {
930 changed_items_++;
931 AddImpl(path);
932 }
933 void SyncDiffReporter::OnRemove(const std::string &path,
934 const catalog::DirectoryEntry & /*entry*/) {
935 changed_items_++;
936 RemoveImpl(path);
937 }
938 void SyncDiffReporter::OnModify(const std::string &path,
939 const catalog::DirectoryEntry & /*entry_from*/,
940 const catalog::DirectoryEntry & /*entry_to*/) {
941 changed_items_++;
942 ModifyImpl(path);
943 }
944
945 void SyncDiffReporter::CommitReport() {
946 if (print_action_ == kPrintDots) {
947 if (changed_items_ >= processing_dot_interval_) {
948 LogCvmfs(kLogPublish, kLogStdout | kLogNoLinebreak, "\n");
949 }
950 }
951 }
952
953 void SyncDiffReporter::PrintDots() {
954 if (changed_items_ % processing_dot_interval_ == 0) {
955 LogCvmfs(kLogPublish, kLogStdout | kLogNoLinebreak, ".");
956 }
957 }
958
959 void SyncDiffReporter::AddImpl(const std::string &path) {
960 const char *action_label;
961
962 switch (print_action_) {
963 case kPrintChanges:
964 if (path.at(0) != '/') {
965 action_label = "[x-catalog-add]";
966 } else {
967 action_label = "[add]";
968 }
969 LogCvmfs(kLogPublish, kLogStdout, "%s %s", action_label, path.c_str());
970 break;
971
972 case kPrintDots:
973 PrintDots();
974 break;
975 default:
976 assert("Invalid print action.");
977 }
978 }
979
980 void SyncDiffReporter::RemoveImpl(const std::string &path) {
981 const char *action_label;
982
983 switch (print_action_) {
984 case kPrintChanges:
985 if (path.at(0) != '/') {
986 action_label = "[x-catalog-rem]";
987 } else {
988 action_label = "[rem]";
989 }
990
991 LogCvmfs(kLogPublish, kLogStdout, "%s %s", action_label, path.c_str());
992 break;
993
994 case kPrintDots:
995 PrintDots();
996 break;
997 default:
998 assert("Invalid print action.");
999 }
1000 }
1001
1002 void SyncDiffReporter::ModifyImpl(const std::string &path) {
1003 const char *action_label;
1004
1005 switch (print_action_) {
1006 case kPrintChanges:
1007 action_label = "[mod]";
1008 LogCvmfs(kLogPublish, kLogStdout, "%s %s", action_label, path.c_str());
1009 break;
1010
1011 case kPrintDots:
1012 PrintDots();
1013 break;
1014 default:
1015 assert("Invalid print action.");
1016 }
1017 }
1018
1019 void SyncMediator::AddFile(SharedPtr<SyncItem> entry) {
1020 reporter_->OnAdd(entry->GetUnionPath(), catalog::DirectoryEntry());
1021
1022 if ((entry->IsSymlink() || entry->IsSpecialFile()) && !params_->dry_run) {
1023 assert(!entry->HasGraftMarker());
1024 // Symlinks and special files are completely stored in the catalog
1025 XattrList *xattrs = &default_xattrs_;
1026 if (params_->include_xattrs) {
1027 xattrs = XattrList::CreateFromFile(entry->GetUnionPath());
1028 assert(xattrs);
1029 }
1030 catalog_manager_->AddFile(
1031 entry->CreateBasicCatalogDirent(params_->enable_mtime_ns),
1032 *xattrs,
1033 entry->relative_parent_path());
1034 if (xattrs != &default_xattrs_)
1035 free(xattrs);
1036 } else if (entry->HasGraftMarker() && !params_->dry_run) {
1037 if (entry->IsValidGraft()) {
1038 // Graft files are added to catalog immediately.
1039 if (entry->IsChunkedGraft()) {
1040 catalog_manager_->AddChunkedFile(
1041 entry->CreateBasicCatalogDirent(params_->enable_mtime_ns),
1042 default_xattrs_,
1043 entry->relative_parent_path(),
1044 *(entry->GetGraftChunks()));
1045 } else {
1046 catalog_manager_->AddFile(
1047 entry->CreateBasicCatalogDirent(params_->enable_mtime_ns),
1048 default_xattrs_, // TODO(bbockelm): For now, use default xattrs
1049 // on grafted files.
1050 entry->relative_parent_path());
1051 }
1052 } else {
1053 // Unlike with regular files, grafted files can be "unpublishable" - i.e.,
1054 // the graft file is missing information. It's not clear that continuing
1055 // forward with the publish is the correct thing to do; abort for now.
1056 PANIC(kLogStderr,
1057 "Encountered a grafted file (%s) with "
1058 "invalid grafting information; check contents of .cvmfsgraft-*"
1059 " file. Aborting publish.",
1060 entry->GetRelativePath().c_str());
1061 }
1062 } else if (entry->relative_parent_path().empty()
1063 && entry->IsCatalogMarker()) {
1064 PANIC(kLogStderr, "Error: nested catalog marker in root directory");
1065 } else if (!params_->dry_run) {
1066 {
1067 // Push the file to the spooler, remember the entry for the path
1068 const MutexLockGuard m(&lock_file_queue_);
1069 file_queue_[entry->GetUnionPath()] = entry;
1070 }
1071 // Spool the file
1072 params_->spooler->Process(entry->CreateIngestionSource());
1073 }
1074
1075 // publish statistics counting for new file
1076 if (entry->IsNew()) {
1077 if (entry->IsSymlink()) {
1078 perf::Inc(counters_->n_symlinks_added);
1079 } else {
1080 perf::Inc(counters_->n_files_added);
1081 perf::Xadd(counters_->sz_added_bytes, entry->GetScratchSize());
1082 }
1083 }
1084 }
1085
1086 void SyncMediator::RemoveFile(SharedPtr<SyncItem> entry) {
1087 reporter_->OnRemove(entry->GetUnionPath(), catalog::DirectoryEntry());
1088
1089 if (!params_->dry_run) {
1090 if (handle_hardlinks_ && entry->GetRdOnlyLinkcount() > 1) {
1091 LogCvmfs(kLogPublish, kLogVerboseMsg, "remove %s from hardlink group",
1092 entry->GetUnionPath().c_str());
1093 catalog_manager_->ShrinkHardlinkGroup(entry->GetRelativePath());
1094 }
1095 catalog_manager_->RemoveFile(entry->GetRelativePath());
1096 }
1097
1098 // Counting nr of removed files and removed bytes
1099 if (entry->WasSymlink()) {
1100 perf::Inc(counters_->n_symlinks_removed);
1101 } else {
1102 perf::Inc(counters_->n_files_removed);
1103 }
1104 perf::Xadd(counters_->sz_removed_bytes, entry->GetRdOnlySize());
1105 }
1106
1107 void SyncMediator::AddUnmaterializedDirectory(SharedPtr<SyncItem> entry) {
1108 AddDirectory(entry);
1109 }
1110
1111 void SyncMediator::AddDirectory(SharedPtr<SyncItem> entry) {
1112 if (entry->IsBundleSpec()) {
1113 PANIC(kLogStderr,
1114 "Illegal directory name: %s. "
1115 "The .cvmfsbundle- prefix is reserved for bundles specifications",
1116 entry->GetUnionPath().c_str());
1117 }
1118
1119 reporter_->OnAdd(entry->GetUnionPath(), catalog::DirectoryEntry());
1120
1121 perf::Inc(counters_->n_directories_added);
1122 assert(!entry->HasGraftMarker());
1123 if (!params_->dry_run) {
1124 XattrList *xattrs = &default_xattrs_;
1125 if (params_->include_xattrs) {
1126 xattrs = XattrList::CreateFromFile(entry->GetUnionPath());
1127 assert(xattrs);
1128 }
1129 catalog_manager_->AddDirectory(
1130 entry->CreateBasicCatalogDirent(params_->enable_mtime_ns), *xattrs,
1131 entry->relative_parent_path());
1132 if (xattrs != &default_xattrs_)
1133 free(xattrs);
1134 }
1135
1136 if (entry->HasCatalogMarker()
1137 && !catalog_manager_->IsTransitionPoint(entry->GetRelativePath())) {
1138 CreateNestedCatalog(entry);
1139 }
1140 }
1141
1142
1143 /**
1144 * this method deletes a single directory entry! Make sure to empty it
1145 * before you call this method or simply use
1146 * SyncMediator::RemoveDirectoryRecursively instead.
1147 */
1148 void SyncMediator::RemoveDirectory(SharedPtr<SyncItem> entry) {
1149 const std::string directory_path = entry->GetRelativePath();
1150
1151 if (catalog_manager_->IsTransitionPoint(directory_path)) {
1152 RemoveNestedCatalog(entry);
1153 }
1154
1155 reporter_->OnRemove(entry->GetUnionPath(), catalog::DirectoryEntry());
1156 if (!params_->dry_run) {
1157 catalog_manager_->RemoveDirectory(directory_path);
1158 }
1159
1160 perf::Inc(counters_->n_directories_removed);
1161 }
1162
1163 void SyncMediator::TouchDirectory(SharedPtr<SyncItem> entry) {
1164 reporter_->OnModify(entry->GetUnionPath(), catalog::DirectoryEntry(),
1165 catalog::DirectoryEntry());
1166
1167 const std::string directory_path = entry->GetRelativePath();
1168
1169 if (!params_->dry_run) {
1170 XattrList *xattrs = &default_xattrs_;
1171 if (params_->include_xattrs) {
1172 xattrs = XattrList::CreateFromFile(entry->GetUnionPath());
1173 assert(xattrs);
1174 }
1175 catalog_manager_->TouchDirectory(
1176 entry->CreateBasicCatalogDirent(params_->enable_mtime_ns), *xattrs,
1177 directory_path);
1178 if (xattrs != &default_xattrs_)
1179 free(xattrs);
1180 }
1181
1182 if (entry->HasCatalogMarker()
1183 && !catalog_manager_->IsTransitionPoint(directory_path)) {
1184 CreateNestedCatalog(entry);
1185 } else if (!entry->HasCatalogMarker()
1186 && catalog_manager_->IsTransitionPoint(directory_path)) {
1187 RemoveNestedCatalog(entry);
1188 }
1189 }
1190
1191 /**
1192 * All hardlinks in the current directory have been picked up. Now they are
1193 * added to the catalogs.
1194 */
1195 void SyncMediator::AddLocalHardlinkGroups(const HardlinkGroupMap &hardlinks) {
1196 assert(handle_hardlinks_);
1197
1198 for (HardlinkGroupMap::const_iterator i = hardlinks.begin(),
1199 iEnd = hardlinks.end();
1200 i != iEnd;
1201 ++i) {
1202 if (i->second.hardlinks.size() != i->second.master->GetUnionLinkcount()
1203 && !params_->ignore_xdir_hardlinks) {
1204 PANIC(kLogSyslogErr | kLogDebug, "Hardlinks across directories (%s)",
1205 i->second.master->GetUnionPath().c_str());
1206 }
1207
1208 if (params_->print_changeset) {
1209 for (SyncItemList::const_iterator j = i->second.hardlinks.begin(),
1210 jEnd = i->second.hardlinks.end();
1211 j != jEnd;
1212 ++j) {
1213 const std::string changeset_notice = GetParentPath(i->second.master
1214 ->GetUnionPath())
1215 + "/" + j->second->filename();
1216 reporter_->OnAdd(changeset_notice, catalog::DirectoryEntry());
1217 }
1218 }
1219
1220 if (params_->dry_run)
1221 continue;
1222
1223 if (i->second.master->IsSymlink() || i->second.master->IsSpecialFile())
1224 AddHardlinkGroup(i->second);
1225 else
1226 hardlink_queue_.push_back(i->second);
1227 }
1228 }
1229
1230
1231 void SyncMediator::AddHardlinkGroup(const HardlinkGroup &group) {
1232 assert(handle_hardlinks_);
1233
1234 // Create a DirectoryEntry list out of the hardlinks
1235 catalog::DirectoryEntryBaseList hardlinks;
1236 for (SyncItemList::const_iterator i = group.hardlinks.begin(),
1237 iEnd = group.hardlinks.end();
1238 i != iEnd;
1239 ++i) {
1240 hardlinks.push_back(
1241 i->second->CreateBasicCatalogDirent(params_->enable_mtime_ns));
1242 }
1243 XattrList *xattrs = &default_xattrs_;
1244 if (params_->include_xattrs) {
1245 xattrs = XattrList::CreateFromFile(group.master->GetUnionPath());
1246 assert(xattrs);
1247 }
1248 catalog_manager_->AddHardlinkGroup(hardlinks,
1249 *xattrs,
1250 group.master->relative_parent_path(),
1251 group.file_chunks);
1252 if (xattrs != &default_xattrs_)
1253 free(xattrs);
1254 }
1255
1256 } // namespace publish
1257