GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/swissknife_ingest.cc
Date: 2026-08-30 02:40:36
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