GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/swissknife_history.cc
Date: 2026-04-26 02:35:59
Exec Total Coverage
Lines: 0 652 0.0%
Branches: 0 1572 0.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System
3 */
4
5 #include "swissknife_history.h"
6
7 #include <algorithm>
8 #include <cassert>
9 #include <ctime>
10
11 #include "catalog_rw.h"
12 #include "crypto/hash.h"
13 #include "history_sqlite.h"
14 #include "manifest_fetch.h"
15 #include "network/download.h"
16 #include "network/sink_path.h"
17 #include "upload.h"
18
19 using namespace std; // NOLINT
20 using namespace swissknife; // NOLINT
21
22 const std::string CommandTag::kHeadTag = "trunk";
23 const std::string CommandTag::kPreviousHeadTag = "trunk-previous";
24
25 const std::string CommandTag::kHeadTagDescription = "current HEAD";
26 const std::string
27 CommandTag::kPreviousHeadTagDescription = "default undo target";
28
29 static void InsertCommonParameters(ParameterList *r) {
30 r->push_back(Parameter::Mandatory('w', "repository directory / url"));
31 r->push_back(Parameter::Mandatory('t', "temporary scratch directory"));
32 r->push_back(Parameter::Optional('p', "public key of the repository"));
33 r->push_back(Parameter::Optional('f', "fully qualified repository name"));
34 r->push_back(Parameter::Optional('r', "spooler definition string"));
35 r->push_back(Parameter::Optional('m', "(unsigned) manifest file to edit"));
36 r->push_back(Parameter::Optional('b', "mounted repository base hash"));
37 r->push_back(
38 Parameter::Optional('e', "hash algorithm to use (default SHA1)"));
39 r->push_back(Parameter::Switch('L', "follow HTTP redirects"));
40 r->push_back(Parameter::Optional('P', "session_token_file"));
41 r->push_back(Parameter::Optional('@', "proxy url"));
42 }
43
44 CommandTag::Environment *CommandTag::InitializeEnvironment(
45 const ArgumentList &args, const bool read_write) {
46 const string repository_url = MakeCanonicalPath(*args.find('w')->second);
47 const string tmp_path = MakeCanonicalPath(*args.find('t')->second);
48 const string spl_definition = (args.find('r') == args.end())
49 ? ""
50 : MakeCanonicalPath(
51 *args.find('r')->second);
52 const string manifest_path = (args.find('m') == args.end())
53 ? ""
54 : MakeCanonicalPath(*args.find('m')->second);
55 const shash::Algorithms hash_algo = (args.find('e') == args.end())
56 ? shash::kSha1
57 : shash::ParseHashAlgorithm(
58 *args.find('e')->second);
59 const string pubkey_path = (args.find('p') == args.end())
60 ? ""
61 : MakeCanonicalPath(*args.find('p')->second);
62 const shash::Any base_hash = (args.find('b') == args.end())
63 ? shash::Any()
64 : shash::MkFromHexPtr(
65 shash::HexPtr(*args.find('b')->second),
66 shash::kSuffixCatalog);
67 const string repo_name = (args.find('f') == args.end())
68 ? ""
69 : *args.find('f')->second;
70
71 string session_token_file;
72 if (args.find('P') != args.end()) {
73 session_token_file = *args.find('P')->second;
74 }
75
76 // Sanity checks
77 if (hash_algo == shash::kAny) {
78 LogCvmfs(kLogCvmfs, kLogStderr, "failed to parse hash algorithm to use");
79 return NULL;
80 }
81
82 if (read_write && spl_definition.empty()) {
83 LogCvmfs(kLogCvmfs, kLogStderr, "no upstream storage provided (-r)");
84 return NULL;
85 }
86
87 if (read_write && manifest_path.empty()) {
88 LogCvmfs(kLogCvmfs, kLogStderr, "no (unsigned) manifest provided (-m)");
89 return NULL;
90 }
91
92 if (!read_write && pubkey_path.empty()) {
93 LogCvmfs(kLogCvmfs, kLogStderr, "no public key provided (-p)");
94 return NULL;
95 }
96
97 if (!read_write && repo_name.empty()) {
98 LogCvmfs(kLogCvmfs, kLogStderr, "no repository name provided (-f)");
99 return NULL;
100 }
101
102 if (HasPrefix(spl_definition, "gw", false)) {
103 if (session_token_file.empty()) {
104 PrintError("Session token file has to be provided "
105 "when upstream type is gw.");
106 return NULL;
107 }
108 }
109
110 // create new environment
111 // Note: We use this encapsulation because we cannot be sure that the
112 // Command object gets deleted properly. With the Environment object at
113 // hand we have full control and can make heavy and safe use of RAII
114 UniquePtr<Environment> env(new Environment(repository_url, tmp_path));
115 env->manifest_path.Set(manifest_path);
116 env->history_path.Set(CreateTempPath(tmp_path + "/history", 0600));
117
118 // initialize the (swissknife global) download manager
119 const bool follow_redirects = (args.count('L') > 0);
120 const std::string &proxy = (args.count('@') > 0) ? *args.find('@')->second
121 : "";
122 if (!this->InitDownloadManager(follow_redirects, proxy)) {
123 return NULL;
124 }
125
126 // initialize the (swissknife global) signature manager (if possible)
127 if (!pubkey_path.empty() && !this->InitSignatureManager(pubkey_path)) {
128 return NULL;
129 }
130
131 // open the (yet unsigned) manifest file if it is there, otherwise load the
132 // latest manifest from the server
133 env->manifest = (FileExists(env->manifest_path.path()))
134 ? OpenLocalManifest(env->manifest_path.path())
135 : FetchRemoteManifest(env->repository_url, repo_name,
136 base_hash);
137
138 if (!env->manifest.IsValid()) {
139 LogCvmfs(kLogCvmfs, kLogStderr, "failed to load manifest file");
140 return NULL;
141 }
142
143 // figure out the hash of the history from the previous revision if needed
144 if (read_write && env->manifest->history().IsNull() && !base_hash.IsNull()) {
145 env->previous_manifest = FetchRemoteManifest(env->repository_url, repo_name,
146 base_hash);
147 if (!env->previous_manifest.IsValid()) {
148 LogCvmfs(kLogCvmfs, kLogStderr, "failed to load previous manifest");
149 return NULL;
150 }
151
152 LogCvmfs(kLogCvmfs, kLogDebug,
153 "using history database '%s' from previous "
154 "manifest (%s) as basis",
155 env->previous_manifest->history().ToString().c_str(),
156 env->previous_manifest->repository_name().c_str());
157 env->manifest->set_history(env->previous_manifest->history());
158 env->manifest->set_repository_name(
159 env->previous_manifest->repository_name());
160 }
161
162 // download the history database referenced in the manifest
163 env->history = GetHistory(env->manifest.weak_ref(), env->repository_url,
164 env->history_path.path(), read_write);
165 if (!env->history.IsValid()) {
166 return NULL;
167 }
168
169 // if the using Command is expected to change the history database, we
170 // need
171 // to initialize the upload spooler for potential later history upload
172 if (read_write) {
173 const bool use_file_chunking = false;
174 const bool generate_legacy_bulk_chunks = false;
175 const upload::SpoolerDefinition sd(
176 spl_definition, hash_algo, zlib::kZlibDefault,
177 generate_legacy_bulk_chunks, use_file_chunking, 0, 0, 0,
178 session_token_file);
179 env->spooler = upload::Spooler::Construct(sd);
180 if (!env->spooler.IsValid()) {
181 LogCvmfs(kLogCvmfs, kLogStderr, "failed to initialize upload spooler");
182 return NULL;
183 }
184 }
185
186 // return the pointer of the Environment (passing the ownership along)
187 return env.Release();
188 }
189
190 bool CommandTag::CloseAndPublishHistory(Environment *env) {
191 assert(env->spooler.IsValid());
192
193 // set the previous revision pointer of the history database
194 env->history->SetPreviousRevision(env->manifest->history());
195
196 // close the history database
197 history::History *weak_history = env->history.Release();
198 delete weak_history;
199
200 // compress and upload the new history database
201 Future<shash::Any> history_hash;
202 upload::Spooler::CallbackPtr callback = env->spooler->RegisterListener(
203 &CommandTag::UploadClosure, this, &history_hash);
204 env->spooler->ProcessHistory(env->history_path.path());
205 env->spooler->WaitForUpload();
206 const shash::Any new_history_hash = history_hash.Get();
207 env->spooler->UnregisterListener(callback);
208
209 // retrieve the (async) uploader result
210 if (new_history_hash.IsNull()) {
211 return false;
212 }
213
214 // update the (yet unsigned) manifest file
215 env->manifest->set_history(new_history_hash);
216 if (!env->manifest->Export(env->manifest_path.path())) {
217 LogCvmfs(kLogCvmfs, kLogStderr, "failed to export the new manifest '%s'",
218 env->manifest_path.path().c_str());
219 return false;
220 }
221
222 // disable the unlink guard in order to keep the newly exported manifest file
223 env->manifest_path.Disable();
224 LogCvmfs(kLogCvmfs, kLogVerboseMsg,
225 "exported manifest (%" PRIu64 ") with new history '%s'",
226 env->manifest->revision(), new_history_hash.ToString().c_str());
227
228 return true;
229 }
230
231
232 bool CommandTag::UploadCatalogAndUpdateManifest(
233 CommandTag::Environment *env, catalog::WritableCatalog *catalog) {
234 assert(env->spooler.IsValid());
235
236 // gather information about catalog to be uploaded and update manifest
237 UniquePtr<catalog::WritableCatalog> wr_catalog(catalog);
238 const std::string catalog_path = wr_catalog->database_path();
239 env->manifest->set_ttl(wr_catalog->GetTTL());
240 env->manifest->set_revision(wr_catalog->GetRevision());
241 env->manifest->set_publish_timestamp(wr_catalog->GetLastModified());
242
243 // close the catalog
244 catalog::WritableCatalog *weak_catalog = wr_catalog.Release();
245 delete weak_catalog;
246
247 // upload the catalog
248 Future<shash::Any> catalog_hash;
249 upload::Spooler::CallbackPtr callback = env->spooler->RegisterListener(
250 &CommandTag::UploadClosure, this, &catalog_hash);
251 env->spooler->ProcessCatalog(catalog_path);
252 env->spooler->WaitForUpload();
253 const shash::Any new_catalog_hash = catalog_hash.Get();
254 env->spooler->UnregisterListener(callback);
255
256 // check if the upload succeeded
257 if (new_catalog_hash.IsNull()) {
258 LogCvmfs(kLogCvmfs, kLogStderr, "failed to upload catalog '%s'",
259 catalog_path.c_str());
260 return false;
261 }
262
263 // update the catalog size and hash in the manifest
264 const size_t catalog_size = GetFileSize(catalog_path);
265 env->manifest->set_catalog_size(catalog_size);
266 env->manifest->set_catalog_hash(new_catalog_hash);
267
268 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "uploaded new catalog (%lu bytes) '%s'",
269 catalog_size, new_catalog_hash.ToString().c_str());
270
271 return true;
272 }
273
274 void CommandTag::UploadClosure(const upload::SpoolerResult &result,
275 Future<shash::Any> *hash) {
276 assert(!result.IsChunked());
277 if (result.return_code != 0) {
278 LogCvmfs(kLogCvmfs, kLogStderr, "failed to upload history database (%d)",
279 result.return_code);
280 hash->Set(shash::Any());
281 } else {
282 hash->Set(result.content_hash);
283 }
284 }
285
286 bool CommandTag::UpdateUndoTags(
287 Environment *env, const history::History::Tag &current_head_template,
288 const bool undo_rollback) {
289 assert(env->history.IsValid());
290
291 history::History::Tag current_head;
292 history::History::Tag current_old_head;
293
294 // remove previous HEAD tag
295 if (!env->history->Remove(CommandTag::kPreviousHeadTag)) {
296 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "didn't find a previous HEAD tag");
297 }
298
299 // check if we have a current HEAD tag that needs to renamed to previous
300 // HEAD
301 if (env->history->GetByName(CommandTag::kHeadTag, &current_head)) {
302 // remove current HEAD tag
303 if (!env->history->Remove(CommandTag::kHeadTag)) {
304 LogCvmfs(kLogCvmfs, kLogStderr, "failed to remove current HEAD tag");
305 return false;
306 }
307
308 // set previous HEAD tag where current HEAD used to be
309 if (!undo_rollback) {
310 current_old_head = current_head;
311 current_old_head.name = CommandTag::kPreviousHeadTag;
312 current_old_head.description = CommandTag::kPreviousHeadTagDescription;
313 if (!env->history->Insert(current_old_head)) {
314 LogCvmfs(kLogCvmfs, kLogStderr, "failed to set previous HEAD tag");
315 return false;
316 }
317 }
318 }
319
320 // set the current HEAD to the catalog provided by the template HEAD
321 current_head = current_head_template;
322 current_head.name = CommandTag::kHeadTag;
323 current_head.description = CommandTag::kHeadTagDescription;
324 if (!env->history->Insert(current_head)) {
325 LogCvmfs(kLogCvmfs, kLogStderr, "failed to set new current HEAD");
326 return false;
327 }
328
329 return true;
330 }
331
332 bool CommandTag::FetchObject(const std::string &repository_url,
333 const shash::Any &object_hash,
334 const std::string &destination_path) const {
335 assert(!object_hash.IsNull());
336
337 download::Failures dl_retval;
338 const std::string url = repository_url + "/data/" + object_hash.MakePath();
339
340 cvmfs::PathSink pathsink(destination_path);
341 download::JobInfo download_object(&url, true, false, &object_hash, &pathsink);
342 dl_retval = download_manager()->Fetch(&download_object);
343
344 if (dl_retval != download::kFailOk) {
345 LogCvmfs(kLogCvmfs, kLogStderr, "failed to download object '%s' (%d - %s)",
346 object_hash.ToStringWithSuffix().c_str(), dl_retval,
347 download::Code2Ascii(dl_retval));
348 return false;
349 }
350
351 return true;
352 }
353
354 history::History *CommandTag::GetHistory(const manifest::Manifest *manifest,
355 const std::string &repository_url,
356 const std::string &history_path,
357 const bool read_write) const {
358 const shash::Any history_hash = manifest->history();
359 history::History *history;
360
361 if (history_hash.IsNull()) {
362 history = history::SqliteHistory::Create(history_path,
363 manifest->repository_name());
364 if (NULL == history) {
365 LogCvmfs(kLogCvmfs, kLogStderr, "failed to create history database");
366 return NULL;
367 }
368 } else {
369 if (!FetchObject(repository_url, history_hash, history_path)) {
370 return NULL;
371 }
372
373 history = (read_write) ? history::SqliteHistory::OpenWritable(history_path)
374 : history::SqliteHistory::Open(history_path);
375 if (NULL == history) {
376 LogCvmfs(kLogCvmfs, kLogStderr, "failed to open history database (%s)",
377 history_path.c_str());
378 unlink(history_path.c_str());
379 return NULL;
380 }
381
382 assert(history->fqrn() == manifest->repository_name());
383 }
384
385 return history;
386 }
387
388 catalog::Catalog *CommandTag::GetCatalog(const std::string &repository_url,
389 const shash::Any &catalog_hash,
390 const std::string catalog_path,
391 const bool read_write) const {
392 assert(shash::kSuffixCatalog == catalog_hash.suffix);
393 if (!FetchObject(repository_url, catalog_hash, catalog_path)) {
394 return NULL;
395 }
396
397 const std::string catalog_root_path = "";
398 return (read_write) ? catalog::WritableCatalog::AttachFreely(
399 catalog_root_path, catalog_path, catalog_hash)
400 : catalog::Catalog::AttachFreely(
401 catalog_root_path, catalog_path, catalog_hash);
402 }
403
404 void CommandTag::PrintTagMachineReadable(
405 const history::History::Tag &tag) const {
406 LogCvmfs(kLogCvmfs, kLogStdout, "%s %s %" PRIu64 " %" PRIu64 " %ld %s %s",
407 tag.name.c_str(), tag.root_hash.ToString().c_str(), tag.size,
408 tag.revision, tag.timestamp,
409 (tag.branch == "") ? "(default)" : tag.branch.c_str(),
410 tag.description.c_str());
411 }
412
413 std::string CommandTag::AddPadding(const std::string &str, const size_t padding,
414 const bool align_right,
415 const std::string &fill_char) const {
416 assert(str.size() <= padding);
417 std::string result(str);
418 result.resize(padding);
419 const size_t pos = (align_right) ? 0 : str.size();
420 const size_t padding_width = padding - str.size();
421 for (size_t i = 0; i < padding_width; ++i)
422 result.insert(pos, fill_char);
423 return result;
424 }
425
426 bool CommandTag::IsUndoTagName(const std::string &tag_name) const {
427 return tag_name == CommandTag::kHeadTag
428 || tag_name == CommandTag::kPreviousHeadTag;
429 }
430
431 //------------------------------------------------------------------------------
432
433 ParameterList CommandEditTag::GetParams() const {
434 ParameterList r;
435 InsertCommonParameters(&r);
436
437 r.push_back(Parameter::Optional('d', "space separated tags to be deleted"));
438 r.push_back(Parameter::Optional('a', "name of the new tag"));
439 r.push_back(Parameter::Optional('D', "description of the tag"));
440 r.push_back(Parameter::Optional('B', "branch of the new tag"));
441 r.push_back(Parameter::Optional('P', "predecessor branch"));
442 r.push_back(Parameter::Optional('h', "root hash of the new tag"));
443 r.push_back(Parameter::Switch('x', "maintain undo tags"));
444 return r;
445 }
446
447 int CommandEditTag::Main(const ArgumentList &args) {
448 if ((args.find('d') == args.end()) && (args.find('a') == args.end())
449 && (args.find('x') == args.end())) {
450 LogCvmfs(kLogCvmfs, kLogStderr, "nothing to do");
451 return 1;
452 }
453
454 // initialize the Environment (taking ownership)
455 const bool history_read_write = true;
456 const UniquePtr<Environment> env(
457 InitializeEnvironment(args, history_read_write));
458 if (!env.IsValid()) {
459 LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
460 return 1;
461 }
462
463 int retval;
464 if (args.find('d') != args.end()) {
465 retval = RemoveTags(args, env.weak_ref());
466 if (retval != 0)
467 return retval;
468 }
469 if ((args.find('a') != args.end()) || (args.find('x') != args.end())) {
470 retval = AddNewTag(args, env.weak_ref());
471 if (retval != 0)
472 return retval;
473 }
474
475 // finalize processing and upload new history database
476 if (!CloseAndPublishHistory(env.weak_ref())) {
477 return 1;
478 }
479 return 0;
480 }
481
482 int CommandEditTag::AddNewTag(const ArgumentList &args, Environment *env) {
483 const std::string tag_name = (args.find('a') != args.end())
484 ? *args.find('a')->second
485 : "";
486 const std::string tag_description = (args.find('D') != args.end())
487 ? *args.find('D')->second
488 : "";
489 const bool undo_tags = (args.find('x') != args.end());
490 const std::string root_hash_string = (args.find('h') != args.end())
491 ? *args.find('h')->second
492 : "";
493 const std::string branch_name = (args.find('B') != args.end())
494 ? *args.find('B')->second
495 : "";
496 const std::string previous_branch_name = (args.find('P') != args.end())
497 ? *args.find('P')->second
498 : "";
499
500 if (tag_name.find(" ") != std::string::npos) {
501 LogCvmfs(kLogCvmfs, kLogStderr, "tag names must not contain spaces");
502 return 1;
503 }
504
505 assert(!tag_name.empty() || undo_tags);
506
507 if (IsUndoTagName(tag_name)) {
508 LogCvmfs(kLogCvmfs, kLogStderr, "undo tags are managed internally");
509 return 1;
510 }
511
512 // set the root hash to be tagged to the current HEAD if no other hash was
513 // given by the user
514 const shash::Any root_hash = GetTagRootHash(env, root_hash_string);
515 if (root_hash.IsNull()) {
516 return 1;
517 }
518
519 // open the catalog to be tagged (to check for existence and for meta info)
520 const UnlinkGuard catalog_path(
521 CreateTempPath(env->tmp_path + "/catalog", 0600));
522 const bool catalog_read_write = false;
523 const UniquePtr<catalog::Catalog> catalog(GetCatalog(
524 env->repository_url, root_hash, catalog_path.path(), catalog_read_write));
525 if (!catalog.IsValid()) {
526 LogCvmfs(kLogCvmfs, kLogStderr, "catalog with hash '%s' does not exist",
527 root_hash.ToString().c_str());
528 return 1;
529 }
530
531 // check if the catalog is a root catalog
532 if (!catalog->root_prefix().IsEmpty()) {
533 LogCvmfs(kLogCvmfs, kLogStderr,
534 "cannot tag catalog '%s' that is not a "
535 "root catalog.",
536 root_hash.ToString().c_str());
537 return 1;
538 }
539
540 // create a template for the new tag to be created, moved or used as undo tag
541 history::History::Tag tag_template;
542 tag_template.name = "<template>";
543 tag_template.root_hash = root_hash;
544 tag_template.size = GetFileSize(catalog_path.path());
545 tag_template.revision = catalog->GetRevision();
546 tag_template.timestamp = catalog->GetLastModified();
547 tag_template.branch = branch_name;
548 tag_template.description = tag_description;
549
550 // manipulate the tag database by creating a new tag or moving an existing one
551 if (!tag_name.empty()) {
552 tag_template.name = tag_name;
553 const bool user_provided_hash = (!root_hash_string.empty());
554
555 if (!env->history->ExistsBranch(tag_template.branch)) {
556 const history::History::Branch branch(
557 tag_template.branch, previous_branch_name, tag_template.revision);
558 if (!env->history->InsertBranch(branch)) {
559 LogCvmfs(kLogCvmfs, kLogStderr, "cannot insert branch '%s'",
560 tag_template.branch.c_str());
561 return 1;
562 }
563 }
564
565 if (!ManipulateTag(env, tag_template, user_provided_hash)) {
566 return 1;
567 }
568 }
569
570 // handle undo tags ('trunk' and 'trunk-previous') if necessary
571 if (undo_tags && !UpdateUndoTags(env, tag_template)) {
572 return 1;
573 }
574
575 return 0;
576 }
577
578 shash::Any CommandEditTag::GetTagRootHash(
579 Environment *env, const std::string &root_hash_string) const {
580 shash::Any root_hash;
581
582 if (root_hash_string.empty()) {
583 LogCvmfs(kLogCvmfs, kLogVerboseMsg,
584 "no catalog hash provided, using hash"
585 "of current HEAD catalog (%s)",
586 env->manifest->catalog_hash().ToString().c_str());
587 root_hash = env->manifest->catalog_hash();
588 } else {
589 root_hash = shash::MkFromHexPtr(shash::HexPtr(root_hash_string),
590 shash::kSuffixCatalog);
591 if (root_hash.IsNull()) {
592 LogCvmfs(kLogCvmfs, kLogStderr,
593 "failed to read provided catalog hash '%s'",
594 root_hash_string.c_str());
595 }
596 }
597
598 return root_hash;
599 }
600
601 bool CommandEditTag::ManipulateTag(Environment *env,
602 const history::History::Tag &tag_template,
603 const bool user_provided_hash) {
604 const std::string &tag_name = tag_template.name;
605
606 // check if the tag already exists, otherwise create it and return
607 if (!env->history->Exists(tag_name)) {
608 return CreateTag(env, tag_template);
609 }
610
611 // tag does exist already, now we need to see if we can move it
612 if (!user_provided_hash) {
613 LogCvmfs(kLogCvmfs, kLogStderr,
614 "a tag with the name '%s' already exists. Do you want to move it? "
615 "(-h <root hash>)",
616 tag_name.c_str());
617 return false;
618 }
619
620 // move the already existing tag and return
621 return MoveTag(env, tag_template);
622 }
623
624 bool CommandEditTag::MoveTag(Environment *env,
625 const history::History::Tag &tag_template) {
626 const std::string &tag_name = tag_template.name;
627 history::History::Tag new_tag = tag_template;
628
629 // get the already existent tag
630 history::History::Tag old_tag;
631 if (!env->history->GetByName(tag_name, &old_tag)) {
632 LogCvmfs(kLogCvmfs, kLogStderr, "failed to retrieve tag '%s' for moving",
633 tag_name.c_str());
634 return false;
635 }
636
637 // check if we would move the tag to the same hash
638 if (old_tag.root_hash == new_tag.root_hash) {
639 LogCvmfs(kLogCvmfs, kLogStderr, "tag '%s' already points to '%s'",
640 tag_name.c_str(), old_tag.root_hash.ToString().c_str());
641 return false;
642 }
643
644 // copy over old description if no new description was given
645 if (new_tag.description.empty()) {
646 new_tag.description = old_tag.description;
647 }
648 new_tag.branch = old_tag.branch;
649
650 // remove the old tag from the database
651 if (!env->history->Remove(tag_name)) {
652 LogCvmfs(kLogCvmfs, kLogStderr, "removing old tag '%s' before move failed",
653 tag_name.c_str());
654 return false;
655 }
656 if (!env->history->PruneBranches()) {
657 LogCvmfs(kLogCvmfs, kLogStderr, "could not prune unused branches");
658 return false;
659 }
660 const bool retval = env->history->Vacuum();
661 assert(retval);
662
663 LogCvmfs(kLogCvmfs, kLogStdout, "moving tag '%s' from '%s' to '%s'",
664 tag_name.c_str(), old_tag.root_hash.ToString().c_str(),
665 tag_template.root_hash.ToString().c_str());
666
667 // re-create the moved tag
668 return CreateTag(env, new_tag);
669 }
670
671 bool CommandEditTag::CreateTag(Environment *env,
672 const history::History::Tag &new_tag) {
673 if (!env->history->Insert(new_tag)) {
674 LogCvmfs(kLogCvmfs, kLogStderr, "failed to insert new tag '%s'",
675 new_tag.name.c_str());
676 return false;
677 }
678
679 return true;
680 }
681
682 int CommandEditTag::RemoveTags(const ArgumentList &args, Environment *env) {
683 typedef std::vector<std::string> TagNames;
684 const std::string tags_to_delete = *args.find('d')->second;
685
686 const TagNames condemned_tags = SplitString(tags_to_delete, ' ');
687
688 // check if user tries to remove a magic undo tag
689 TagNames::const_iterator i = condemned_tags.begin();
690 const TagNames::const_iterator iend = condemned_tags.end();
691 for (; i != iend; ++i) {
692 if (IsUndoTagName(*i)) {
693 LogCvmfs(kLogCvmfs, kLogStderr,
694 "undo tags are handled internally and cannot be deleted");
695 return 1;
696 }
697 }
698
699 LogCvmfs(kLogCvmfs, kLogDebug, "proceeding to delete %lu tags",
700 condemned_tags.size());
701
702 // check if the tags to be deleted exist
703 bool all_exist = true;
704 for (i = condemned_tags.begin(); i != iend; ++i) {
705 if (!env->history->Exists(*i)) {
706 LogCvmfs(kLogCvmfs, kLogStderr, "tag '%s' does not exist", i->c_str());
707 all_exist = false;
708 }
709 }
710 if (!all_exist) {
711 return 1;
712 }
713
714 // delete the tags from the tag database and print their root hashes
715 i = condemned_tags.begin();
716 env->history->BeginTransaction();
717 for (; i != iend; ++i) {
718 // print some information about the tag to be deleted
719 history::History::Tag condemned_tag;
720 const bool found_tag = env->history->GetByName(*i, &condemned_tag);
721 assert(found_tag);
722 LogCvmfs(kLogCvmfs, kLogStdout, "deleting '%s' (%s)",
723 condemned_tag.name.c_str(),
724 condemned_tag.root_hash.ToString().c_str());
725
726 // remove the tag
727 if (!env->history->Remove(*i)) {
728 LogCvmfs(kLogCvmfs, kLogStderr, "failed to remove tag '%s' from history",
729 i->c_str());
730 return 1;
731 }
732 }
733 bool retval = env->history->PruneBranches();
734 if (!retval) {
735 LogCvmfs(kLogCvmfs, kLogStderr,
736 "failed to prune unused branches from history");
737 return 1;
738 }
739 env->history->CommitTransaction();
740 retval = env->history->Vacuum();
741 assert(retval);
742
743 return 0;
744 }
745
746 //------------------------------------------------------------------------------
747
748
749 ParameterList CommandListTags::GetParams() const {
750 ParameterList r;
751 InsertCommonParameters(&r);
752 r.push_back(Parameter::Switch('x', "machine readable output"));
753 r.push_back(Parameter::Switch('B', "print branch hierarchy"));
754 return r;
755 }
756
757 void CommandListTags::PrintHumanReadableTagList(
758 const CommandListTags::TagList &tags) const {
759 // go through the list of tags and figure out the column widths
760 const std::string name_label = "Name";
761 const std::string rev_label = "Revision";
762 const std::string time_label = "Timestamp";
763 const std::string branch_label = "Branch";
764 const std::string desc_label = "Description";
765
766 // figure out the maximal lengths of the fields in the lists
767 TagList::const_reverse_iterator i = tags.rbegin();
768 const TagList::const_reverse_iterator iend = tags.rend();
769 size_t max_name_len = name_label.size();
770 size_t max_rev_len = rev_label.size();
771 size_t max_time_len = desc_label.size();
772 size_t max_branch_len = branch_label.size();
773 for (; i != iend; ++i) {
774 max_name_len = std::max(max_name_len, i->name.size());
775 max_rev_len = std::max(max_rev_len, StringifyInt(i->revision).size());
776 max_time_len = std::max(max_time_len,
777 StringifyTime(i->timestamp, true).size());
778 max_branch_len = std::max(max_branch_len, i->branch.size());
779 }
780
781 // print the list header
782 LogCvmfs(kLogCvmfs, kLogStdout, "%s \u2502 %s \u2502 %s \u2502 %s \u2502 %s",
783 AddPadding(name_label, max_name_len).c_str(),
784 AddPadding(rev_label, max_rev_len).c_str(),
785 AddPadding(time_label, max_time_len).c_str(),
786 AddPadding(branch_label, max_branch_len).c_str(),
787 desc_label.c_str());
788 LogCvmfs(kLogCvmfs, kLogStdout,
789 "%s\u2500\u253C\u2500%s\u2500\u253C\u2500%s"
790 "\u2500\u253C\u2500%s\u2500\u253C\u2500%s",
791 AddPadding("", max_name_len, false, "\u2500").c_str(),
792 AddPadding("", max_rev_len, false, "\u2500").c_str(),
793 AddPadding("", max_time_len, false, "\u2500").c_str(),
794 AddPadding("", max_branch_len, false, "\u2500").c_str(),
795 AddPadding("", desc_label.size() + 1, false, "\u2500").c_str());
796
797 // print the rows of the list
798 i = tags.rbegin();
799 for (; i != iend; ++i) {
800 LogCvmfs(
801 kLogCvmfs, kLogStdout, "%s \u2502 %s \u2502 %s \u2502 %s \u2502 %s",
802 AddPadding(i->name, max_name_len).c_str(),
803 AddPadding(StringifyInt(i->revision), max_rev_len, true).c_str(),
804 AddPadding(StringifyTime(i->timestamp, true), max_time_len).c_str(),
805 AddPadding(i->branch, max_branch_len).c_str(), i->description.c_str());
806 }
807
808 // print the list footer
809 LogCvmfs(kLogCvmfs, kLogStdout,
810 "%s\u2500\u2534\u2500%s\u2500\u2534\u2500%s"
811 "\u2500\u2534\u2500%s\u2500\u2534\u2500%s",
812 AddPadding("", max_name_len, false, "\u2500").c_str(),
813 AddPadding("", max_rev_len, false, "\u2500").c_str(),
814 AddPadding("", max_time_len, false, "\u2500").c_str(),
815 AddPadding("", max_branch_len, false, "\u2500").c_str(),
816 AddPadding("", desc_label.size() + 1, false, "\u2500").c_str());
817
818 // print the number of tags listed
819 LogCvmfs(kLogCvmfs, kLogStdout, "listing contains %lu tags", tags.size());
820 }
821
822 void CommandListTags::PrintMachineReadableTagList(const TagList &tags) const {
823 TagList::const_iterator i = tags.begin();
824 const TagList::const_iterator iend = tags.end();
825 for (; i != iend; ++i) {
826 PrintTagMachineReadable(*i);
827 }
828 }
829
830
831 void CommandListTags::PrintHumanReadableBranchList(
832 const BranchHierarchy &branches) const {
833 const unsigned N = branches.size();
834 for (unsigned i = 0; i < N; ++i) {
835 for (unsigned l = 0; l < branches[i].level; ++l) {
836 LogCvmfs(kLogCvmfs, kLogStdout | kLogNoLinebreak, "%s",
837 ((l + 1) == branches[i].level) ? "\u251c " : "\u2502 ");
838 }
839 LogCvmfs(kLogCvmfs, kLogStdout, "%s @%" PRIu64,
840 branches[i].branch.branch.c_str(),
841 branches[i].branch.initial_revision);
842 }
843 }
844
845
846 void CommandListTags::PrintMachineReadableBranchList(
847 const BranchHierarchy &branches) const {
848 const unsigned N = branches.size();
849 for (unsigned i = 0; i < N; ++i) {
850 LogCvmfs(kLogCvmfs, kLogStdout, "[%u] %s%s @%" PRIu64, branches[i].level,
851 AddPadding("", branches[i].level, false, " ").c_str(),
852 branches[i].branch.branch.c_str(),
853 branches[i].branch.initial_revision);
854 }
855 }
856
857
858 void CommandListTags::SortBranchesRecursively(
859 unsigned level,
860 const string &parent_branch,
861 const BranchList &branches,
862 BranchHierarchy *hierarchy) const {
863 // For large numbers of branches, this should be turned into the O(n) version
864 // using a linked list
865 const unsigned N = branches.size();
866 for (unsigned i = 0; i < N; ++i) {
867 if (branches[i].branch == "")
868 continue;
869 if (branches[i].parent == parent_branch) {
870 hierarchy->push_back(BranchLevel(branches[i], level));
871 SortBranchesRecursively(level + 1, branches[i].branch, branches,
872 hierarchy);
873 }
874 }
875 }
876
877
878 CommandListTags::BranchHierarchy CommandListTags::SortBranches(
879 const BranchList &branches) const {
880 BranchHierarchy hierarchy;
881 hierarchy.push_back(
882 BranchLevel(history::History::Branch("(default)", "", 0), 0));
883 SortBranchesRecursively(1, "", branches, &hierarchy);
884 return hierarchy;
885 }
886
887
888 int CommandListTags::Main(const ArgumentList &args) {
889 const bool machine_readable = (args.find('x') != args.end());
890 const bool branch_hierarchy = (args.find('B') != args.end());
891
892 // initialize the Environment (taking ownership)
893 const bool history_read_write = false;
894 const UniquePtr<Environment> env(
895 InitializeEnvironment(args, history_read_write));
896 if (!env.IsValid()) {
897 LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
898 return 1;
899 }
900
901 if (branch_hierarchy) {
902 BranchList branch_list;
903 if (!env->history->ListBranches(&branch_list)) {
904 LogCvmfs(kLogCvmfs, kLogStderr,
905 "failed to list branches in history database");
906 return 1;
907 }
908 const BranchHierarchy branch_hierarchy = SortBranches(branch_list);
909
910 if (machine_readable) {
911 PrintMachineReadableBranchList(branch_hierarchy);
912 } else {
913 PrintHumanReadableBranchList(branch_hierarchy);
914 }
915 } else {
916 // obtain a full list of all tags
917 TagList tags;
918 if (!env->history->List(&tags)) {
919 LogCvmfs(kLogCvmfs, kLogStderr,
920 "failed to list tags in history database");
921 return 1;
922 }
923
924 if (machine_readable) {
925 PrintMachineReadableTagList(tags);
926 } else {
927 PrintHumanReadableTagList(tags);
928 }
929 }
930
931 return 0;
932 }
933
934 //------------------------------------------------------------------------------
935
936 ParameterList CommandInfoTag::GetParams() const {
937 ParameterList r;
938 InsertCommonParameters(&r);
939
940 r.push_back(Parameter::Mandatory('n', "name of the tag to be inspected"));
941 r.push_back(Parameter::Switch('x', "machine readable output"));
942 return r;
943 }
944
945 std::string CommandInfoTag::HumanReadableFilesize(const size_t filesize) const {
946 const size_t kiB = 1024;
947 const size_t MiB = kiB * 1024;
948 const size_t GiB = MiB * 1024;
949
950 if (filesize > GiB) {
951 return StringifyDouble(static_cast<double>(filesize) / GiB) + " GiB";
952 } else if (filesize > MiB) {
953 return StringifyDouble(static_cast<double>(filesize) / MiB) + " MiB";
954 } else if (filesize > kiB) {
955 return StringifyDouble(static_cast<double>(filesize) / kiB) + " kiB";
956 } else {
957 return StringifyInt(filesize) + " Byte";
958 }
959 }
960
961 void CommandInfoTag::PrintHumanReadableInfo(
962 const history::History::Tag &tag) const {
963 LogCvmfs(kLogCvmfs, kLogStdout,
964 "Name: %s\n"
965 "Revision: %" PRIu64 "\n"
966 "Timestamp: %s\n"
967 "Branch: %s\n"
968 "Root Hash: %s\n"
969 "Catalog Size: %s\n"
970 "%s",
971 tag.name.c_str(), tag.revision,
972 StringifyTime(tag.timestamp, true /* utc */).c_str(),
973 tag.branch.c_str(), tag.root_hash.ToString().c_str(),
974 HumanReadableFilesize(tag.size).c_str(), tag.description.c_str());
975 }
976
977 int CommandInfoTag::Main(const ArgumentList &args) {
978 const std::string tag_name = *args.find('n')->second;
979 const bool machine_readable = (args.find('x') != args.end());
980
981 // initialize the Environment (taking ownership)
982 const bool history_read_write = false;
983 const UniquePtr<Environment> env(
984 InitializeEnvironment(args, history_read_write));
985 if (!env.IsValid()) {
986 LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
987 return 1;
988 }
989
990 history::History::Tag tag;
991 const bool found = env->history->GetByName(tag_name, &tag);
992 if (!found) {
993 LogCvmfs(kLogCvmfs, kLogStderr, "tag '%s' does not exist",
994 tag_name.c_str());
995 return 1;
996 }
997
998 if (machine_readable) {
999 PrintTagMachineReadable(tag);
1000 } else {
1001 PrintHumanReadableInfo(tag);
1002 }
1003
1004 return 0;
1005 }
1006
1007 //------------------------------------------------------------------------------
1008
1009 ParameterList CommandRollbackTag::GetParams() const {
1010 ParameterList r;
1011 InsertCommonParameters(&r);
1012
1013 r.push_back(Parameter::Optional('n', "name of the tag to be republished"));
1014 return r;
1015 }
1016
1017 int CommandRollbackTag::Main(const ArgumentList &args) {
1018 const bool undo_rollback = (args.find('n') == args.end());
1019 const std::string tag_name = (!undo_rollback) ? *args.find('n')->second
1020 : CommandTag::kPreviousHeadTag;
1021
1022 // initialize the Environment (taking ownership)
1023 const bool history_read_write = true;
1024 const UniquePtr<Environment> env(
1025 InitializeEnvironment(args, history_read_write));
1026 if (!env.IsValid()) {
1027 LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
1028 return 1;
1029 }
1030
1031 // find tag to be rolled back to
1032 history::History::Tag target_tag;
1033 const bool found = env->history->GetByName(tag_name, &target_tag);
1034 if (!found) {
1035 if (undo_rollback) {
1036 LogCvmfs(kLogCvmfs, kLogStderr,
1037 "only one anonymous rollback supported - "
1038 "perhaps you want to provide a tag name?");
1039 } else {
1040 LogCvmfs(kLogCvmfs, kLogStderr, "tag '%s' does not exist",
1041 tag_name.c_str());
1042 }
1043 return 1;
1044 }
1045 if (target_tag.branch != "") {
1046 LogCvmfs(kLogCvmfs, kLogStderr,
1047 "rollback is only supported on the default branch");
1048 return 1;
1049 }
1050
1051 // list the tags that will be deleted
1052 TagList affected_tags;
1053 if (!env->history->ListTagsAffectedByRollback(tag_name, &affected_tags)) {
1054 LogCvmfs(kLogCvmfs, kLogStderr,
1055 "failed to list condemned tags prior to rollback to '%s'",
1056 tag_name.c_str());
1057 return 1;
1058 }
1059
1060 // check if tag is valid to be rolled back to
1061 const uint64_t current_revision = env->manifest->revision();
1062 assert(target_tag.revision <= current_revision);
1063 if (target_tag.revision == current_revision) {
1064 LogCvmfs(kLogCvmfs, kLogStderr,
1065 "not rolling back to current head (%" PRIu64 ")",
1066 current_revision);
1067 return 1;
1068 }
1069
1070 // open the catalog to be rolled back to
1071 const UnlinkGuard catalog_path(
1072 CreateTempPath(env->tmp_path + "/catalog", 0600));
1073 const bool catalog_read_write = true;
1074 UniquePtr<catalog::WritableCatalog> catalog(
1075 dynamic_cast<catalog::WritableCatalog *>(
1076 GetCatalog(env->repository_url, target_tag.root_hash,
1077 catalog_path.path(), catalog_read_write)));
1078 if (!catalog.IsValid()) {
1079 LogCvmfs(kLogCvmfs, kLogStderr, "failed to open catalog with hash '%s'",
1080 target_tag.root_hash.ToString().c_str());
1081 return 1;
1082 }
1083
1084 // check if the catalog has a supported schema version
1085 if (catalog->schema() < catalog::CatalogDatabase::kLatestSupportedSchema
1086 - catalog::CatalogDatabase::kSchemaEpsilon) {
1087 LogCvmfs(kLogCvmfs, kLogStderr,
1088 "not rolling back to outdated and "
1089 "incompatible catalog schema (%.1f < %.1f)",
1090 catalog->schema(),
1091 catalog::CatalogDatabase::kLatestSupportedSchema);
1092 return 1;
1093 }
1094
1095 // update the catalog to be republished
1096 catalog->Transaction();
1097 catalog->UpdateLastModified();
1098 catalog->SetRevision(current_revision + 1);
1099 catalog->SetPreviousRevision(env->manifest->catalog_hash());
1100 catalog->Commit();
1101
1102 // Upload catalog (handing over ownership of catalog pointer)
1103 if (!UploadCatalogAndUpdateManifest(env.weak_ref(), catalog.Release())) {
1104 LogCvmfs(kLogCvmfs, kLogStderr, "catalog upload failed");
1105 return 1;
1106 }
1107
1108 // update target tag with newly published root catalog information
1109 history::History::Tag updated_target_tag(target_tag);
1110 updated_target_tag.root_hash = env->manifest->catalog_hash();
1111 updated_target_tag.size = env->manifest->catalog_size();
1112 updated_target_tag.revision = env->manifest->revision();
1113 updated_target_tag.timestamp = env->manifest->publish_timestamp();
1114 if (!env->history->Rollback(updated_target_tag)) {
1115 LogCvmfs(kLogCvmfs, kLogStderr, "failed to rollback history to '%s'",
1116 updated_target_tag.name.c_str());
1117 return 1;
1118 }
1119 const bool retval = env->history->Vacuum();
1120 assert(retval);
1121
1122 // set the magic undo tags
1123 if (!UpdateUndoTags(env.weak_ref(), updated_target_tag, undo_rollback)) {
1124 LogCvmfs(kLogCvmfs, kLogStderr, "failed to update magic undo tags");
1125 return 1;
1126 }
1127
1128 // finalize the history and upload it
1129 if (!CloseAndPublishHistory(env.weak_ref())) {
1130 return 1;
1131 }
1132
1133 // print the tags that have been removed by the rollback
1134 PrintDeletedTagList(affected_tags);
1135
1136 return 0;
1137 }
1138
1139 void CommandRollbackTag::PrintDeletedTagList(const TagList &tags) const {
1140 size_t longest_name = 0;
1141 TagList::const_iterator i = tags.begin();
1142 const TagList::const_iterator iend = tags.end();
1143 for (; i != iend; ++i) {
1144 longest_name = std::max(i->name.size(), longest_name);
1145 }
1146
1147 i = tags.begin();
1148 for (; i != iend; ++i) {
1149 LogCvmfs(kLogCvmfs, kLogStdout, "removed tag %s (%s)",
1150 AddPadding(i->name, longest_name).c_str(),
1151 i->root_hash.ToString().c_str());
1152 }
1153 }
1154
1155 //------------------------------------------------------------------------------
1156
1157 ParameterList CommandEmptyRecycleBin::GetParams() const {
1158 ParameterList r;
1159 InsertCommonParameters(&r);
1160 return r;
1161 }
1162
1163 int CommandEmptyRecycleBin::Main(const ArgumentList &args) {
1164 // initialize the Environment (taking ownership)
1165 const bool history_read_write = true;
1166 const UniquePtr<Environment> env(
1167 InitializeEnvironment(args, history_read_write));
1168 if (!env.IsValid()) {
1169 LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
1170 return 1;
1171 }
1172
1173 if (!env->history->EmptyRecycleBin()) {
1174 LogCvmfs(kLogCvmfs, kLogStderr, "failed to empty recycle bin");
1175 return 1;
1176 }
1177
1178 // finalize the history and upload it
1179 if (!CloseAndPublishHistory(env.weak_ref())) {
1180 return 1;
1181 }
1182
1183 return 0;
1184 }
1185