GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/swissknife_ingest.cc
Date: 2026-10-04 02:40:33
Exec Total Coverage
Lines: 0 156 0.0%
Branches: 0 92 0.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System
3 */
4
5 #include "swissknife_ingest.h"
6
7 #include <fcntl.h>
8 #include <unistd.h>
9
10 #include <memory>
11 #include <string>
12 #include <vector>
13
14 #include "catalog_virtual.h"
15 #include "manifest.h"
16 #include "statistics.h"
17 #include "statistics_database.h"
18 #include "swissknife_ingest_gc.h"
19 #include "sync_mediator.h"
20 #include "sync_union.h"
21 #include "sync_union_tarball.h"
22 #include "util/capabilities.h"
23 #include "util/logging.h"
24 #include "util/posix.h"
25
26 /*
27 * Many of the options possible to set in the ArgumentList are not actually used
28 * by the ingest command since they are not part of its interface, hence those
29 * unused options cannot be set by the shell script. Of course if there is the
30 * necessitty those parameters can be added and managed.
31 * At the moment this approach worked fine and didn't add much complexity,
32 * however if yet another command will need to use a similar approach it would
33 * be good to consider creating different options handler for each command.
34 */
35 ✗ int swissknife::Ingest::Main(const swissknife::ArgumentList &args) {
36 ✗ const std::string start_time = GetGMTimestamp();
37
38 ✗ SyncParameters params;
39 ✗ params.dir_rdonly = MakeCanonicalPath(*args.find('c')->second);
40 ✗ params.dir_temp = MakeCanonicalPath(*args.find('t')->second);
41 ✗ params.base_hash = shash::MkFromHexPtr(shash::HexPtr(*args.find('b')->second),
42 shash::kSuffixCatalog);
43 ✗ params.stratum0 = *args.find('w')->second;
44 ✗ params.manifest_path = *args.find('o')->second;
45 ✗ params.spooler_definition = *args.find('r')->second;
46
47 ✗ params.public_keys = *args.find('K')->second;
48 ✗ params.repo_name = *args.find('N')->second;
49
50 ✗ if (args.find('T') != args.end()) {
51 ✗ params.tar_file = *args.find('T')->second;
52 }
53 ✗ if (args.find('B') != args.end()) {
54 ✗ params.base_directory = *args.find('B')->second;
55 }
56 ✗ if (args.find('D') != args.end()) {
57 ✗ params.to_delete = *args.find('D')->second;
58 }
59
60 ✗ std::string gc_db_path;
61 ✗ std::vector<int64_t> gc_batch_ids;
62 ✗ if (args.find('Q') != args.end()) {
63 ✗ gc_db_path = *args.find('Q')->second;
64 // Batch size comes from the -X flag (populated by the shell from
65 // CVMFS_GC_DB_BATCH_SIZE in the repository server.conf). 0 / missing /
66 // negative means "no limit": read all pending rows. Default is 1000.
67 ✗ int gc_db_batch_size = 1000;
68 ✗ if (args.find('X') != args.end()) {
69 ✗ gc_db_batch_size = static_cast<int>(
70 ✗ String2Int64(*args.find('X')->second));
71 ✗ if (gc_db_batch_size < 0)
72 ✗ gc_db_batch_size = 0;
73 }
74 ✗ std::vector<std::string> gc_paths;
75 ✗ if (!ReadGCDatabase(gc_db_path, &gc_paths, &gc_batch_ids,
76 gc_db_batch_size)) {
77 ✗ PrintError("Swissknife Ingest: failed to read GC database");
78 ✗ return 1;
79 }
80 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
81 "Swissknife Ingest: Read %lu paths from GC database %s "
82 "(batch size %d)",
83 gc_paths.size(), gc_db_path.c_str(), gc_db_batch_size);
84 // Append GC paths to the existing to_delete string using /// delimiter
85 ✗ for (size_t i = 0; i < gc_paths.size(); ++i) {
86 ✗ if (!params.to_delete.empty()) {
87 ✗ params.to_delete += "///";
88 }
89 ✗ params.to_delete += gc_paths[i];
90 }
91 // GC database implies fast delete
92 ✗ if (!gc_paths.empty()) {
93 ✗ params.fast_delete = true;
94 }
95 }
96
97 ✗ if (args.find('O') != args.end()) {
98 ✗ params.generate_legacy_bulk_chunks = true;
99 }
100 ✗ if (args.find('j') != args.end()) {
101 ✗ params.enable_mtime_ns = true;
102 }
103 ✗ if (args.find('f') != args.end()) {
104 ✗ params.fast_delete = true;
105 }
106 ✗ if (args.find('m') != args.end()) {
107 ✗ params.tolerate_missing_hardlinks = true;
108 }
109 ✗ shash::Algorithms hash_algorithm = shash::kSha1;
110 ✗ if (args.find('e') != args.end()) {
111 ✗ hash_algorithm = shash::ParseHashAlgorithm(*args.find('e')->second);
112 ✗ if (hash_algorithm == shash::kAny) {
113 ✗ PrintError("Swissknife Ingest: unknown hash algorithm");
114 ✗ return 1;
115 }
116 }
117 ✗ if (args.find('Z') != args.end()) {
118 ✗ params.compression_alg = zlib::ParseCompressionAlgorithm(
119 ✗ *args.find('Z')->second);
120 }
121 ✗ if (args.find('U') != args.end()) {
122 ✗ params.uid = static_cast<uid_t>(String2Int64(*args.find('U')->second));
123 }
124 ✗ if (args.find('G') != args.end()) {
125 ✗ params.gid = static_cast<gid_t>(String2Int64(*args.find('G')->second));
126 }
127
128 ✗ const bool create_catalog = args.find('C') != args.end();
129
130 ✗ params.nested_kcatalog_limit = SyncParameters::kDefaultNestedKcatalogLimit;
131 ✗ params.root_kcatalog_limit = SyncParameters::kDefaultRootKcatalogLimit;
132 ✗ params.file_mbyte_limit = SyncParameters::kDefaultFileMbyteLimit;
133
134 ✗ params.branched_catalog = false; // could be true?
135
136 ✗ if (args.find('P') != args.end()) {
137 ✗ params.session_token_file = *args.find('P')->second;
138 }
139
140 ✗ if (args.find('H') != args.end()) {
141 ✗ params.key_file = *args.find('H')->second;
142 }
143
144 ✗ const bool upload_statsdb = (args.count('I') > 0);
145
146 ✗ perf::StatisticsTemplate publish_statistics("publish", this->statistics());
147 ✗ StatisticsDatabase *stats_db = StatisticsDatabase::OpenStandardDB(
148 params.repo_name);
149
150 upload::SpoolerDefinition spooler_definition(
151 params.spooler_definition, hash_algorithm, params.compression_alg,
152 ✗ params.generate_legacy_bulk_chunks, params.use_file_chunking,
153 params.min_file_chunk_size, params.avg_file_chunk_size,
154 ✗ params.max_file_chunk_size, params.session_token_file, params.key_file);
155 ✗ if (params.max_concurrent_write_jobs > 0) {
156 spooler_definition
157 ✗ .number_of_concurrent_uploads = params.max_concurrent_write_jobs;
158 }
159
160 // Sanitize base_directory, removing any leading or trailing slashes
161 // from non-root (!= "/") paths
162 ✗ params.base_directory = TrimString(params.base_directory, "/", kTrimAll);
163
164 const upload::SpoolerDefinition spooler_definition_catalogs(
165 ✗ spooler_definition.Dup2DefaultCompression());
166
167 ✗ params.spooler = upload::Spooler::Construct(spooler_definition,
168 &publish_statistics);
169 ✗ if (NULL == params.spooler)
170 ✗ return 3;
171 const std::unique_ptr<upload::Spooler> spooler_catalogs(
172 upload::Spooler::Construct(spooler_definition_catalogs,
173 ✗ &publish_statistics));
174 ✗ if (spooler_catalogs.get() == nullptr)
175 ✗ return 3;
176
177 ✗ const bool follow_redirects = (args.count('L') > 0);
178 ✗ const string proxy = (args.count('@') > 0) ? *args.find('@')->second : "";
179 ✗ if (!InitDownloadManager(follow_redirects, proxy)) {
180 ✗ return 3;
181 }
182
183 ✗ if (!InitSignatureManager(params.public_keys)) {
184 ✗ return 3;
185 }
186
187 ✗ const bool with_gateway = spooler_definition.driver_type
188 == upload::SpoolerDefinition::Gateway;
189
190 // This may fail, in which case a warning is printed and the process continues
191 ✗ ObtainDacReadSearchCapability();
192
193 ✗ std::unique_ptr<manifest::Manifest> manifest;
194 ✗ if (params.branched_catalog) {
195 // Throw-away manifest
196 ✗ manifest.reset(new manifest::Manifest(shash::Any(), 0, ""));
197 } else {
198 ✗ if (with_gateway) {
199 ✗ manifest.reset(
200 ✗ FetchRemoteManifest(params.stratum0, params.repo_name, shash::Any()));
201 } else {
202 ✗ manifest.reset(FetchRemoteManifest(params.stratum0, params.repo_name,
203 params.base_hash));
204 }
205 }
206 ✗ if (manifest.get() == nullptr) {
207 ✗ return 3;
208 }
209
210 ✗ const std::string old_root_hash = manifest->catalog_hash().ToString(true);
211
212 catalog::WritableCatalogManager catalog_manager(
213 params.base_hash, params.stratum0, params.dir_temp,
214 ✗ spooler_catalogs.get(), download_manager(), params.enforce_limits,
215 params.nested_kcatalog_limit, params.root_kcatalog_limit,
216 ✗ params.file_mbyte_limit, statistics(), params.is_balanced,
217 ✗ params.max_weight, params.min_weight);
218 ✗ catalog_manager.Init();
219
220 ✗ publish::SyncMediator mediator(&catalog_manager, &params, publish_statistics);
221 ✗ LogCvmfs(kLogPublish, kLogStdout, "Swissknife Ingest: Processing changes...");
222
223 publish::SyncUnion *sync = new publish::SyncUnionTarball(
224 &mediator, params.dir_rdonly, params.tar_file, params.base_directory,
225 params.uid, params.gid, params.to_delete, create_catalog,
226 ✗ params.fast_delete, "///", params.tolerate_missing_hardlinks);
227
228 ✗ if (!sync->Initialize()) {
229 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
230 "Swissknife Ingest: Initialization of the synchronisation "
231 "engine failed");
232 ✗ return 4;
233 }
234
235 ✗ sync->Traverse();
236
237 ✗ if (!params.authz_file.empty()) {
238 LogCvmfs(kLogCvmfs, kLogDebug,
239 "Swissknife Ingest: Adding contents of authz file %s to"
240 " root catalog.",
241 params.authz_file.c_str());
242 ✗ const int fd = open(params.authz_file.c_str(), O_RDONLY);
243 ✗ if (fd == -1) {
244 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
245 "Swissknife Ingest: Unable to open authz file (%s)"
246 "from the publication process: %s",
247 params.authz_file.c_str(), strerror(errno));
248 ✗ return 7;
249 }
250
251 ✗ std::string new_authz;
252 ✗ const bool read_successful = SafeReadToString(fd, &new_authz);
253 ✗ close(fd);
254
255 ✗ if (!read_successful) {
256 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
257 "Swissknife Ingest: Failed to read authz file (%s): %s",
258 params.authz_file.c_str(), strerror(errno));
259 ✗ return 8;
260 }
261
262 ✗ catalog_manager.SetVOMSAuthz(new_authz);
263 }
264
265 ✗ if (!mediator.Commit(manifest.get())) {
266 ✗ PrintError("Swissknife Ingest: something went wrong during sync");
267 ✗ stats_db->StorePublishStatistics(this->statistics(), start_time, false);
268 ✗ if (upload_statsdb) {
269 ✗ stats_db->UploadStatistics(params.spooler);
270 }
271 ✗ return 5;
272 }
273
274 ✗ perf::Counter *revision_counter = statistics()->Register(
275 "publish.revision", "Published revision number");
276 ✗ revision_counter->Set(catalog_manager.GetRootCatalog()->revision());
277
278 // finalize the spooler
279 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
280 "Swissknife Ingest: Wait for all uploads to finish");
281 ✗ params.spooler->WaitForUpload();
282 ✗ spooler_catalogs->WaitForUpload();
283 ✗ params.spooler->FinalizeSession(false);
284
285 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
286 "Swissknife Ingest: Exporting repository manifest");
287
288 // We call FinalizeSession(true) this time, to also trigger the commit
289 // operation on the gateway machine (if the upstream is of type "gw").
290
291 // Get the path of the new root catalog
292 ✗ const std::string new_root_hash = manifest->catalog_hash().ToString(true);
293
294 ✗ if (!spooler_catalogs->FinalizeSession(true, old_root_hash, new_root_hash,
295 params.repo_tag)) {
296 ✗ PrintError("Swissknife Ingest: Failed to commit the transaction.");
297 ✗ stats_db->StorePublishStatistics(this->statistics(), start_time, false);
298 ✗ if (upload_statsdb) {
299 ✗ stats_db->UploadStatistics(params.spooler);
300 }
301 ✗ return 9;
302 }
303
304 ✗ stats_db->StorePublishStatistics(this->statistics(), start_time, true);
305 ✗ if (upload_statsdb) {
306 ✗ stats_db->UploadStatistics(params.spooler);
307 }
308
309 ✗ delete params.spooler;
310
311 ✗ if (!manifest->Export(params.manifest_path)) {
312 ✗ PrintError("Swissknife Ingest: Failed to create new repository");
313 ✗ return 6;
314 }
315
316 // Mark GC paths as deleted in the database after successful publication.
317 // Only the rows that were just read (gc_batch_ids) are marked, so rows
318 // added after the read by a concurrent scanner are preserved for the next
319 // invocation.
320 ✗ if (!gc_db_path.empty()) {
321 ✗ if (MarkGCPathsDeleted(gc_db_path, gc_batch_ids)) {
322 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
323 "Swissknife Ingest: Marked %lu GC paths as deleted in %s",
324 gc_batch_ids.size(), gc_db_path.c_str());
325 } else {
326 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
327 "Swissknife Ingest: WARNING - Failed to mark GC paths as "
328 "deleted in %s. Paths were removed from the repository but "
329 "the database was not updated.",
330 gc_db_path.c_str());
331 }
332 }
333
334 ✗ return 0;
335 }
336