diff --git a/doc/xml/release.xml b/doc/xml/release.xml
index d10894426..87067244c 100644
--- a/doc/xml/release.xml
+++ b/doc/xml/release.xml
@@ -63,6 +63,14 @@
Enable S3 storage and encryption for archive-get command in C.
+
+
+
+
+
+ Migrate local info command to C.
+
+
Add S3 storage driver.
diff --git a/src/Makefile b/src/Makefile
index 19c5d4e98..aa9d41954 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -183,6 +183,9 @@ command/control/control.o: command/control/control.c command/control/control.h c
command/help/help.o: command/help/help.c common/assert.h common/debug.h common/error.auto.h common/error.h common/io/handle.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h version.h
$(CC) $(CFLAGS) -c command/help/help.c -o command/help/help.o
+command/info/info.o: command/info/info.c command/archive/common.h command/info/info.h common/debug.h common/error.auto.h common/error.h common/ini.h common/io/filter/filter.h common/io/filter/group.h common/io/handle.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/json.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h crypto/crypto.h crypto/hash.h info/info.h info/infoArchive.h info/infoBackup.h info/infoPg.h perl/exec.h postgres/interface.h storage/fileRead.h storage/fileWrite.h storage/helper.h storage/info.h storage/storage.h
+ $(CC) $(CFLAGS) -c command/info/info.c -o command/info/info.o
+
common/debug.o: common/debug.c common/debug.h common/logLevel.h common/stackTrace.h common/type/convert.h
$(CC) $(CFLAGS) -c common/debug.c -o common/debug.o
diff --git a/src/command/archive/common.c b/src/command/archive/common.c
index 3c859d1d4..ca85c5ae6 100644
--- a/src/command/archive/common.c
+++ b/src/command/archive/common.c
@@ -21,6 +21,8 @@ WAL segment constants
***********************************************************************************************************************************/
STRING_EXTERN(WAL_SEGMENT_REGEXP_STR, WAL_SEGMENT_REGEXP);
STRING_EXTERN(WAL_SEGMENT_PARTIAL_REGEXP_STR, WAL_SEGMENT_PARTIAL_REGEXP);
+STRING_EXTERN(WAL_SEGMENT_DIR_REGEXP_STR, WAL_SEGMENT_DIR_REGEXP);
+STRING_EXTERN(WAL_SEGMENT_FILE_REGEXP_STR, WAL_SEGMENT_FILE_REGEXP);
/***********************************************************************************************************************************
Check for ok/error status files in the spool in/out directory
diff --git a/src/command/archive/common.h b/src/command/archive/common.h
index eaba41a02..efce1dcea 100644
--- a/src/command/archive/common.h
+++ b/src/command/archive/common.h
@@ -40,6 +40,12 @@ WAL segment constants
// Defines the size of standard WAL segment name -- hopefully this won't change
#define WAL_SEGMENT_NAME_SIZE ((unsigned int)24)
+// WAL segment directory/file
+#define WAL_SEGMENT_DIR_REGEXP "^[0-F]{16}$"
+ STRING_DECLARE(WAL_SEGMENT_DIR_REGEXP_STR);
+#define WAL_SEGMENT_FILE_REGEXP "^[0-F]{24}-[0-f]{40}(\\.gz){0,1}$"
+ STRING_DECLARE(WAL_SEGMENT_FILE_REGEXP_STR);
+
/***********************************************************************************************************************************
Functions
***********************************************************************************************************************************/
diff --git a/src/command/info/info.c b/src/command/info/info.c
new file mode 100644
index 000000000..81b7359e6
--- /dev/null
+++ b/src/command/info/info.c
@@ -0,0 +1,643 @@
+/***********************************************************************************************************************************
+Info Command
+***********************************************************************************************************************************/
+#include
+#include
+#include
+#include
+
+#include "command/archive/common.h"
+#include "command/info/info.h"
+#include "common/debug.h"
+#include "common/io/handle.h"
+#include "common/log.h"
+#include "common/memContext.h"
+#include "common/type/json.h"
+#include "config/config.h"
+#include "info/info.h"
+#include "info/infoArchive.h"
+#include "info/infoBackup.h"
+#include "info/infoPg.h"
+#include "perl/exec.h"
+#include "postgres/interface.h"
+#include "storage/helper.h"
+
+/***********************************************************************************************************************************
+Constants
+***********************************************************************************************************************************/
+STRING_STATIC(CFGOPTVAL_INFO_OUTPUT_TEXT_STR, "text");
+
+// Naming convention: _KEY__STR. If the key exists in multiple sections, then _ is omitted.
+STRING_STATIC(ARCHIVE_KEY_MIN_STR, "min");
+STRING_STATIC(ARCHIVE_KEY_MAX_STR, "max");
+STRING_STATIC(BACKREST_KEY_FORMAT_STR, "format");
+STRING_STATIC(BACKREST_KEY_VERSION_STR, "version");
+STRING_STATIC(BACKUP_KEY_BACKREST_STR, "backrest");
+STRING_STATIC(BACKUP_KEY_INFO_STR, "info");
+STRING_STATIC(BACKUP_KEY_LABEL_STR, "label");
+STRING_STATIC(BACKUP_KEY_PRIOR_STR, "prior");
+STRING_STATIC(BACKUP_KEY_REFERENCE_STR, "reference");
+STRING_STATIC(BACKUP_KEY_TIMESTAMP_STR, "timestamp");
+STRING_STATIC(BACKUP_KEY_TYPE_STR, "type");
+STRING_STATIC(DB_KEY_ID_STR, "id");
+STRING_STATIC(DB_KEY_SYSTEM_ID_STR, "system-id");
+STRING_STATIC(DB_KEY_VERSION_STR, "version");
+STRING_STATIC(INFO_KEY_REPOSITORY_STR, "repository");
+STRING_STATIC(KEY_ARCHIVE_STR, "archive");
+STRING_STATIC(KEY_DATABASE_STR, "database");
+STRING_STATIC(KEY_DELTA_STR, "delta");
+STRING_STATIC(KEY_SIZE_STR, "size");
+STRING_STATIC(KEY_START_STR, "start");
+STRING_STATIC(KEY_STOP_STR, "stop");
+STRING_STATIC(STANZA_KEY_BACKUP_STR, "backup");
+STRING_STATIC(STANZA_KEY_CIPHER_STR, "cipher");
+STRING_STATIC(STANZA_KEY_NAME_STR, "name");
+STRING_STATIC(STANZA_KEY_STATUS_STR, "status");
+STRING_STATIC(STANZA_KEY_DB_STR, "db");
+STRING_STATIC(STATUS_KEY_CODE_STR, "code");
+STRING_STATIC(STATUS_KEY_MESSAGE_STR, "message");
+
+STRING_STATIC(INFO_STANZA_STATUS_OK, "ok");
+STRING_STATIC(INFO_STANZA_STATUS_ERROR, "error");
+#define INFO_STANZA_STATUS_CODE_OK 0
+STRING_STATIC(INFO_STANZA_STATUS_MESSAGE_OK_STR, "ok");
+#define INFO_STANZA_STATUS_CODE_MISSING_STANZA_PATH 1
+STRING_STATIC(INFO_STANZA_STATUS_MESSAGE_MISSING_STANZA_PATH_STR, "missing stanza path");
+#define INFO_STANZA_STATUS_CODE_NO_BACKUP 2
+STRING_STATIC(INFO_STANZA_STATUS_MESSAGE_NO_BACKUP_STR, "no valid backups");
+#define INFO_STANZA_STATUS_CODE_MISSING_STANZA_DATA 3
+STRING_STATIC(INFO_STANZA_STATUS_MESSAGE_MISSING_STANZA_DATA_STR, "missing stanza data");
+
+/***********************************************************************************************************************************
+Set error status code and message for the stanza to the code and message passed.
+***********************************************************************************************************************************/
+static void
+stanzaStatus(const int code, const String *message, Variant *stanzaInfo)
+{
+ FUNCTION_TEST_BEGIN();
+ FUNCTION_TEST_PARAM(INT, code);
+ FUNCTION_TEST_PARAM(STRING, message);
+ FUNCTION_TEST_PARAM(VARIANT, stanzaInfo);
+
+ FUNCTION_TEST_ASSERT(code >= 0 && code <= 3);
+ FUNCTION_TEST_ASSERT(message != NULL);
+ FUNCTION_TEST_ASSERT(stanzaInfo != NULL);
+ FUNCTION_TEST_END();
+
+ Variant *stanzaStatus = varNewStr(STANZA_KEY_STATUS_STR);
+ KeyValue *statusKv = kvPutKv(varKv(stanzaInfo), stanzaStatus);
+
+ kvAdd(statusKv, varNewStr(STATUS_KEY_CODE_STR), varNewInt(code));
+ kvAdd(statusKv, varNewStr(STATUS_KEY_MESSAGE_STR), varNewStr(message));
+
+ FUNCTION_TEST_RESULT_VOID();
+}
+
+/***********************************************************************************************************************************
+Set the data for the archive section of the stanza for the database info from the backup.info file.
+***********************************************************************************************************************************/
+void
+archiveDbList(const String *stanza, const InfoPgData *pgData, VariantList *archiveSection, const InfoArchive *info, bool currentDb)
+{
+ FUNCTION_TEST_BEGIN();
+ FUNCTION_TEST_PARAM(STRING, stanza);
+ FUNCTION_TEST_PARAM(INFO_PG_DATAP, pgData);
+ FUNCTION_TEST_PARAM(VARIANT, archiveSection);
+ FUNCTION_TEST_PARAM(BOOL, currentDb);
+
+ FUNCTION_TEST_ASSERT(stanza != NULL);
+ FUNCTION_TEST_ASSERT(pgData != NULL);
+ FUNCTION_TEST_ASSERT(archiveSection != NULL);
+ FUNCTION_TEST_END();
+
+ // With multiple DB versions, the backup.info history-id may not be the same as archive.info history-id, so the
+ // archive path must be built by retrieving the archive id given the db version and system id of the backup.info file.
+ // If there is no match, an error will be thrown.
+ const String *archiveId = infoArchiveIdHistoryMatch(info, pgData->id, pgData->version, pgData->systemId);
+
+ String *archivePath = strNewFmt("%s/%s/%s", STORAGE_REPO_ARCHIVE, strPtr(stanza), strPtr(archiveId));
+ String *archiveStart = NULL;
+ String *archiveStop = NULL;
+ Variant *archiveInfo = varNewKv();
+
+ // Get a list of WAL directories in the archive repo from oldest to newest, if any exist
+ StringList *walDir = storageListP(storageRepo(), archivePath, .expression = WAL_SEGMENT_DIR_REGEXP_STR);
+
+ if (walDir != NULL)
+ {
+ unsigned int sizeWalDir = strLstSize(walDir);
+
+ if (sizeWalDir > 1)
+ walDir = strLstSort(walDir, sortOrderAsc);
+
+ // Not every WAL dir has WAL files so check each
+ for (unsigned int idx = 0; idx < sizeWalDir; idx++)
+ {
+ // Get a list of all WAL in this WAL dir
+ StringList *list = storageListP(
+ storageRepo(), strNewFmt("%s/%s", strPtr(archivePath), strPtr(strLstGet(walDir, idx))),
+ .expression = WAL_SEGMENT_FILE_REGEXP_STR);
+
+ // If wal segments are found, get the oldest one as the archive start
+ if (strLstSize(list) > 0)
+ {
+ // Sort the list from oldest to newest to get the oldest starting WAL archived for this DB
+ list = strLstSort(list, sortOrderAsc);
+ archiveStart = strSubN(strLstGet(list, 0), 0, 24);
+ break;
+ }
+ }
+
+ // Iterate through the directory list in the reverse so processing newest first. Cast comparison to an int for readability.
+ for (unsigned int idx = sizeWalDir - 1; (int)idx > 0; idx--)
+ {
+ // Get a list of all WAL in this WAL dir
+ StringList *list = storageListP(
+ storageRepo(), strNewFmt("%s/%s", strPtr(archivePath), strPtr(strLstGet(walDir, idx))),
+ .expression = WAL_SEGMENT_FILE_REGEXP_STR);
+
+ // If wal segments are found, get the newest one as the archive stop
+ if (strLstSize(list) > 0)
+ {
+ // Sort the list from newest to oldest to get the newest ending WAL archived for this DB
+ list = strLstSort(list, sortOrderDesc);
+ archiveStop = strSubN(strLstGet(list, 0), 0, 24);
+ break;
+ }
+ }
+ }
+
+ // If there is an archive or the database is the current database then store it
+ if (currentDb || archiveStart != NULL)
+ {
+ // Add empty database section to archiveInfo and then fill in database id from the backup.info
+ KeyValue *databaseInfo = kvPutKv(varKv(archiveInfo), varNewStr(KEY_DATABASE_STR));
+
+ kvAdd(databaseInfo, varNewStr(DB_KEY_ID_STR), varNewUInt64(pgData->id));
+
+ kvPut(varKv(archiveInfo), varNewStr(DB_KEY_ID_STR), varNewStr(archiveId));
+ kvPut(
+ varKv(archiveInfo), varNewStr(ARCHIVE_KEY_MIN_STR), (archiveStart != NULL ? varNewStr(archiveStart) : (Variant *)NULL));
+ kvPut(varKv(archiveInfo), varNewStr(ARCHIVE_KEY_MAX_STR), (archiveStop != NULL ? varNewStr(archiveStop) : (Variant *)NULL));
+
+ varLstAdd(archiveSection, archiveInfo);
+ }
+
+ FUNCTION_TEST_RESULT_VOID();
+}
+
+/***********************************************************************************************************************************
+For each current backup in the backup.info file of the stanza, set the data for the backup section.
+***********************************************************************************************************************************/
+void
+backupList(const String *stanza, VariantList *backupSection, InfoBackup *info)
+{
+ FUNCTION_TEST_BEGIN();
+ FUNCTION_TEST_PARAM(STRING, stanza);
+ FUNCTION_TEST_PARAM(VARIANT, backupSection);
+ FUNCTION_TEST_PARAM(INFO_BACKUP, info);
+
+ FUNCTION_TEST_ASSERT(stanza != NULL);
+ FUNCTION_TEST_ASSERT(backupSection != NULL);
+ FUNCTION_TEST_ASSERT(info != NULL);
+ FUNCTION_TEST_END();
+
+ // For each current backup, get the label and corresponding data and build the backup section
+ for (unsigned int keyIdx = 0; keyIdx < infoBackupDataTotal(info); keyIdx++)
+ {
+ // Get the backup data
+ InfoBackupData backupData = infoBackupData(info, keyIdx);
+
+ Variant *backupInfo = varNewKv();
+
+ // main keys
+ kvPut(varKv(backupInfo), varNewStr(BACKUP_KEY_LABEL_STR), varNewStr(backupData.backupLabel));
+ kvPut(varKv(backupInfo), varNewStr(BACKUP_KEY_TYPE_STR), varNewStr(backupData.backupType));
+ kvPut(
+ varKv(backupInfo), varNewStr(BACKUP_KEY_PRIOR_STR),
+ (backupData.backupPrior != NULL ? varNewStr(backupData.backupPrior) : NULL));
+ kvPut(
+ varKv(backupInfo), varNewStr(BACKUP_KEY_REFERENCE_STR),
+ (backupData.backupReference != NULL ? varNewVarLst(varLstNewStrLst(backupData.backupReference)) : NULL));
+
+ // archive section
+ KeyValue *archiveInfo = kvPutKv(varKv(backupInfo), varNewStr(KEY_ARCHIVE_STR));
+
+ kvAdd(
+ archiveInfo, varNewStr(KEY_START_STR),
+ (backupData.backupArchiveStart != NULL ? varNewStr(backupData.backupArchiveStart) : NULL));
+ kvAdd(
+ archiveInfo, varNewStr(KEY_STOP_STR),
+ (backupData.backupArchiveStop != NULL ? varNewStr(backupData.backupArchiveStop) : NULL));
+
+ // backrest section
+ KeyValue *backrestInfo = kvPutKv(varKv(backupInfo), varNewStr(BACKUP_KEY_BACKREST_STR));
+
+ kvAdd(backrestInfo, varNewStr(BACKREST_KEY_FORMAT_STR), varNewUInt64(backupData.backrestFormat));
+ kvAdd(backrestInfo, varNewStr(BACKREST_KEY_VERSION_STR), varNewStr(backupData.backrestVersion));
+
+ // database section
+ KeyValue *dbInfo = kvPutKv(varKv(backupInfo), varNewStr(KEY_DATABASE_STR));
+
+ kvAdd(dbInfo, varNewStr(DB_KEY_ID_STR), varNewUInt64(backupData.backupPgId));
+
+ // info section
+ KeyValue *infoInfo = kvPutKv(varKv(backupInfo), varNewStr(BACKUP_KEY_INFO_STR));
+
+ kvAdd(infoInfo, varNewStr(KEY_SIZE_STR), varNewUInt64(backupData.backupInfoSize));
+ kvAdd(infoInfo, varNewStr(KEY_DELTA_STR), varNewUInt64(backupData.backupInfoSizeDelta));
+
+ // info:repository section
+ KeyValue *repoInfo = kvPutKv(infoInfo, varNewStr(INFO_KEY_REPOSITORY_STR));
+
+ kvAdd(repoInfo, varNewStr(KEY_SIZE_STR), varNewUInt64(backupData.backupInfoRepoSize));
+ kvAdd(repoInfo, varNewStr(KEY_DELTA_STR), varNewUInt64(backupData.backupInfoRepoSizeDelta));
+
+ // timestamp section
+ KeyValue *timeInfo = kvPutKv(varKv(backupInfo), varNewStr(BACKUP_KEY_TIMESTAMP_STR));
+
+ kvAdd(timeInfo, varNewStr(KEY_START_STR), varNewUInt64(backupData.backupTimestampStart));
+ kvAdd(timeInfo, varNewStr(KEY_STOP_STR), varNewUInt64(backupData.backupTimestampStop));
+
+ varLstAdd(backupSection, backupInfo);
+ }
+
+
+ FUNCTION_TEST_RESULT_VOID();
+}
+
+/***********************************************************************************************************************************
+Set the stanza data for each stanza found in the repo.
+***********************************************************************************************************************************/
+static VariantList *
+stanzaInfoList(const String *stanza, StringList *stanzaList)
+{
+ FUNCTION_TEST_BEGIN();
+ FUNCTION_TEST_PARAM(STRING, stanza);
+ FUNCTION_TEST_PARAM(STRING_LIST, stanzaList);
+
+ FUNCTION_TEST_ASSERT(stanzaList != NULL);
+ FUNCTION_TEST_END();
+
+ VariantList *result = varLstNew();
+ bool stanzaFound = false;
+
+ // Sort the list
+ stanzaList = strLstSort(stanzaList, sortOrderAsc);
+
+ for (unsigned int idx = 0; idx < strLstSize(stanzaList); idx++)
+ {
+ String *stanzaListName = strLstGet(stanzaList, idx);
+
+ // If a specific stanza has been requested and this is not it, then continue to the next in the list else indicate found
+ if (stanza != NULL)
+ {
+ if (!strEq(stanza, stanzaListName))
+ continue;
+ else
+ stanzaFound = true;
+ }
+
+ // Create the stanzaInfo and section variables
+ Variant *stanzaInfo = varNewKv();
+ VariantList *dbSection = varLstNew();
+ VariantList *backupSection = varLstNew();
+ VariantList *archiveSection = varLstNew();
+ InfoBackup *info = NULL;
+
+ // Catch certain errors
+ TRY_BEGIN()
+ {
+ // Attempt to load the backup info file
+ info = infoBackupNew(
+ storageRepo(), strNewFmt("%s/%s/%s", STORAGE_REPO_BACKUP, strPtr(stanzaListName), INFO_BACKUP_FILE), false,
+ cipherType(cfgOptionStr(cfgOptRepoCipherType)), cfgOptionStr(cfgOptRepoCipherPass));
+ }
+ CATCH(FileMissingError)
+ {
+ // If there is no backup.info then set the status to indicate missing
+ stanzaStatus(
+ INFO_STANZA_STATUS_CODE_MISSING_STANZA_DATA, INFO_STANZA_STATUS_MESSAGE_MISSING_STANZA_DATA_STR, stanzaInfo);
+ }
+ CATCH(CryptoError)
+ {
+ // If a reason for the error is due to a an ecryption error, add a hint
+ THROW_FMT(
+ CryptoError,
+ "%s\n"
+ "HINT: use option --stanza if encryption settings are different for the stanza than the global settings",
+ errorMessage());
+ }
+ TRY_END();
+
+ // Set the stanza name and cipher
+ kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_NAME_STR), varNewStr(stanzaListName));
+ kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_CIPHER_STR), varNewStr(cfgOptionStr(cfgOptRepoCipherType)));
+
+ // If the backup.info file exists, get the database history information (newest to oldest) and corresponding archive
+ if (info != NULL)
+ {
+ for (unsigned int pgIdx = 0; pgIdx < infoPgDataTotal(infoBackupPg(info)); pgIdx++)
+ {
+ InfoPgData pgData = infoPgData(infoBackupPg(info), pgIdx);
+ Variant *pgInfo = varNewKv();
+
+ kvPut(varKv(pgInfo), varNewStr(DB_KEY_ID_STR), varNewUInt64(pgData.id));
+ kvPut(varKv(pgInfo), varNewStr(DB_KEY_SYSTEM_ID_STR), varNewUInt64(pgData.systemId));
+ kvPut(varKv(pgInfo), varNewStr(DB_KEY_VERSION_STR), varNewStr(pgVersionToStr(pgData.version)));
+
+ varLstAdd(dbSection, pgInfo);
+
+ // Get the archive info for the DB from the archive.info file
+ InfoArchive *info = infoArchiveNew(
+ storageRepo(), strNewFmt("%s/%s/%s", STORAGE_REPO_ARCHIVE, strPtr(stanzaListName), INFO_ARCHIVE_FILE), false,
+ cipherType(cfgOptionStr(cfgOptRepoCipherType)), cfgOptionStr(cfgOptRepoCipherPass));
+ archiveDbList(stanzaListName, &pgData, archiveSection, info, (pgIdx == 0 ? true : false));
+ }
+
+ // Get data for all existing backups for this stanza
+ backupList(stanzaListName, backupSection, info);
+ }
+
+ // Add the database history, backup and archive sections to the stanza info
+ kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_DB_STR), varNewVarLst(dbSection));
+ kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_BACKUP_STR), varNewVarLst(backupSection));
+ kvPut(varKv(stanzaInfo), varNewStr(KEY_ARCHIVE_STR), varNewVarLst(archiveSection));
+
+ // If a status has not already been set and there are no backups then set status to no backup
+ if (kvGet(varKv(stanzaInfo), varNewStr(STANZA_KEY_STATUS_STR)) == NULL &&
+ varLstSize(kvGetList(varKv(stanzaInfo), varNewStr(STANZA_KEY_BACKUP_STR))) == 0)
+ {
+ stanzaStatus(INFO_STANZA_STATUS_CODE_NO_BACKUP, INFO_STANZA_STATUS_MESSAGE_NO_BACKUP_STR, stanzaInfo);
+ }
+
+ // If a status has still not been set then set it to OK
+ if (kvGet(varKv(stanzaInfo), varNewStr(STANZA_KEY_STATUS_STR)) == NULL)
+ stanzaStatus(INFO_STANZA_STATUS_CODE_OK, INFO_STANZA_STATUS_MESSAGE_OK_STR, stanzaInfo);
+
+ varLstAdd(result, stanzaInfo);
+ }
+
+ // If looking for a specific stanza and it was not found, set minimum info and the status
+ if (stanza != NULL && !stanzaFound)
+ {
+ Variant *stanzaInfo = varNewKv();
+
+ kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_NAME_STR), varNewStr(stanza));
+
+ kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_DB_STR), varNewVarLst(varLstNew()));
+ kvPut(varKv(stanzaInfo), varNewStr(STANZA_KEY_BACKUP_STR), varNewVarLst(varLstNew()));
+
+ stanzaStatus(INFO_STANZA_STATUS_CODE_MISSING_STANZA_PATH, INFO_STANZA_STATUS_MESSAGE_MISSING_STANZA_PATH_STR, stanzaInfo);
+ varLstAdd(result, stanzaInfo);
+ }
+
+ FUNCTION_TEST_RESULT(VARIANT_LIST, result);
+}
+
+/***********************************************************************************************************************************
+Format the text output for each database of the stanza.
+***********************************************************************************************************************************/
+static void
+formatTextDb(const KeyValue *stanzaInfo, String *resultStr)
+{
+ FUNCTION_TEST_BEGIN();
+ FUNCTION_TEST_PARAM(KEY_VALUE, stanzaInfo);
+ FUNCTION_TEST_PARAM(STRING, resultStr);
+
+ FUNCTION_TEST_ASSERT(stanzaInfo != NULL);
+ FUNCTION_TEST_END();
+
+ VariantList *dbSection = kvGetList(stanzaInfo, varNewStr(STANZA_KEY_DB_STR));
+ VariantList *archiveSection = kvGetList(stanzaInfo, varNewStr(KEY_ARCHIVE_STR));
+ VariantList *backupSection = kvGetList(stanzaInfo, varNewStr(STANZA_KEY_BACKUP_STR));
+
+ // For each database find the corresponding archive and backup info
+ for (unsigned int dbIdx = 0; dbIdx < varLstSize(dbSection); dbIdx++)
+ {
+ KeyValue *pgInfo = varKv(varLstGet(dbSection, dbIdx));
+ uint64_t dbId = varUInt64(kvGet(pgInfo, varNewStr(DB_KEY_ID_STR)));
+
+ // List is ordered so 0 is always the current DB index
+ if (dbIdx == 0)
+ strCat(resultStr, "\n db (current)");
+
+ // Get the min/max archive information for the database
+ String *archiveResult = strNew("");
+
+ for (unsigned int archiveIdx = 0; archiveIdx < varLstSize(archiveSection); archiveIdx++)
+ {
+ KeyValue *archiveInfo = varKv(varLstGet(archiveSection, archiveIdx));
+ KeyValue *archiveDbInfo = varKv(kvGet(archiveInfo, varNewStr(KEY_DATABASE_STR)));
+ uint64_t archiveDbId = varUInt64(kvGet(archiveDbInfo, varNewStr(DB_KEY_ID_STR)));
+
+ if (archiveDbId == dbId)
+ {
+ strCatFmt(
+ archiveResult, "\n wal archive min/max (%s): ",
+ strPtr(varStr(kvGet(archiveInfo, varNewStr(DB_KEY_ID_STR)))));
+
+ // Get the archive min/max if there are any archives for the database
+ if (kvGet(archiveInfo, varNewStr(ARCHIVE_KEY_MIN_STR)) != NULL)
+ {
+ strCatFmt(
+ archiveResult, "%s/%s\n", strPtr(varStr(kvGet(archiveInfo, varNewStr(ARCHIVE_KEY_MIN_STR)))),
+ strPtr(varStr(kvGet(archiveInfo, varNewStr(ARCHIVE_KEY_MAX_STR)))));
+ }
+ else
+ strCat(archiveResult, "none present\n");
+ }
+ }
+
+ // Get the information for each current backup
+ String *backupResult = strNew("");
+
+ for (unsigned int backupIdx = 0; backupIdx < varLstSize(backupSection); backupIdx++)
+ {
+ KeyValue *backupInfo = varKv(varLstGet(backupSection, backupIdx));
+ KeyValue *backupDbInfo = varKv(kvGet(backupInfo, varNewStr(KEY_DATABASE_STR)));
+ uint64_t backupDbId = varUInt64(kvGet(backupDbInfo, varNewStr(DB_KEY_ID_STR)));
+
+ if (backupDbId == dbId)
+ {
+ strCatFmt(
+ backupResult, "\n %s backup: %s\n", strPtr(varStr(kvGet(backupInfo, varNewStr(BACKUP_KEY_TYPE_STR)))),
+ strPtr(varStr(kvGet(backupInfo, varNewStr(BACKUP_KEY_LABEL_STR)))));
+
+ KeyValue *timestampInfo = varKv(kvGet(backupInfo, varNewStr(BACKUP_KEY_TIMESTAMP_STR)));
+
+ // Get and format the backup start/stop time
+ static char timeBufferStart[20];
+ static char timeBufferStop[20];
+ time_t timeStart = (time_t) varUInt64(kvGet(timestampInfo, varNewStr(KEY_START_STR)));
+ time_t timeStop = (time_t) varUInt64(kvGet(timestampInfo, varNewStr(KEY_STOP_STR)));
+
+ strftime(timeBufferStart, 20, "%Y-%m-%d %H:%M:%S", localtime(&timeStart));
+ strftime(timeBufferStop, 20, "%Y-%m-%d %H:%M:%S", localtime(&timeStop));
+
+ strCatFmt(
+ backupResult, " timestamp start/stop: %s / %s\n", timeBufferStart, timeBufferStop);
+ strCat(backupResult, " wal start/stop: ");
+
+ KeyValue *archiveDBackupInfo = varKv(kvGet(backupInfo, varNewStr(KEY_ARCHIVE_STR)));
+
+ if (kvGet(archiveDBackupInfo, varNewStr(KEY_START_STR)) != NULL &&
+ kvGet(archiveDBackupInfo, varNewStr(KEY_STOP_STR)) != NULL)
+ {
+ strCatFmt(backupResult, "%s / %s\n", strPtr(varStr(kvGet(archiveDBackupInfo, varNewStr(KEY_START_STR)))),
+ strPtr(varStr(kvGet(archiveDBackupInfo, varNewStr(KEY_STOP_STR)))));
+ }
+ else
+ strCat(backupResult, "n/a\n");
+
+ KeyValue *info = varKv(kvGet(backupInfo, varNewStr(BACKUP_KEY_INFO_STR)));
+
+ strCatFmt(
+ backupResult, " database size: %s, backup size: %s\n",
+ strPtr(strSizeFormat(varUInt64Force(kvGet(info, varNewStr(KEY_SIZE_STR))))),
+ strPtr(strSizeFormat(varUInt64Force(kvGet(info, varNewStr(KEY_DELTA_STR))))));
+
+ KeyValue *repoInfo = varKv(kvGet(info, varNewStr(INFO_KEY_REPOSITORY_STR)));
+
+ strCatFmt(
+ backupResult, " repository size: %s, repository backup size: %s\n",
+ strPtr(strSizeFormat(varUInt64Force(kvGet(repoInfo, varNewStr(KEY_SIZE_STR))))),
+ strPtr(strSizeFormat(varUInt64Force(kvGet(repoInfo, varNewStr(KEY_DELTA_STR))))));
+
+ if (kvGet(backupInfo, varNewStr(BACKUP_KEY_REFERENCE_STR)) != NULL)
+ {
+ StringList *referenceList = strLstNewVarLst(varVarLst(kvGet(backupInfo, varNewStr(BACKUP_KEY_REFERENCE_STR))));
+ strCatFmt(backupResult, " backup reference list: %s\n", strPtr(strLstJoin(referenceList, ", ")));
+ }
+ }
+ }
+
+ // If there is data to display, then display it.
+ if (strSize(archiveResult) > 0 || strSize(backupResult) > 0)
+ {
+ if (dbIdx != 0)
+ strCat(resultStr, "\n db (prior)");
+
+ if (strSize(archiveResult) > 0)
+ strCat(resultStr, strPtr(archiveResult));
+
+ if (strSize(backupResult) > 0)
+ strCat(resultStr, strPtr(backupResult));
+ }
+ }
+ FUNCTION_TEST_RESULT_VOID();
+}
+
+/***********************************************************************************************************************************
+Render the information for the stanza based on the command parameters.
+***********************************************************************************************************************************/
+static String *
+infoRender(void)
+{
+ FUNCTION_DEBUG_VOID(logLevelDebug);
+
+ String *result = NULL;
+
+ MEM_CONTEXT_TEMP_BEGIN()
+ {
+ // Get stanza if specified
+ const String *stanza = cfgOptionTest(cfgOptStanza) ? cfgOptionStr(cfgOptStanza) : NULL;
+
+ // Get a list of stanzas in the backup directory.
+ StringList *stanzaList = storageListP(storageRepo(), strNew(STORAGE_REPO_BACKUP), .errorOnMissing = true);
+ VariantList *infoList = varLstNew();
+ String *resultStr = strNew("");
+
+ // If the backup storage exists, then search for and process any stanzas
+ if (strLstSize(stanzaList) > 0)
+ infoList = stanzaInfoList(stanza, stanzaList);
+
+ // Format text output
+ if (strEq(cfgOptionStr(cfgOptOutput), CFGOPTVAL_INFO_OUTPUT_TEXT_STR))
+ {
+ // Process any stanza directories
+ if (varLstSize(infoList) > 0)
+ {
+ for (unsigned int stanzaIdx = 0; stanzaIdx < varLstSize(infoList); stanzaIdx++)
+ {
+ KeyValue *stanzaInfo = varKv(varLstGet(infoList, stanzaIdx));
+
+ // Add a carriage return between stanzas
+ if (stanzaIdx > 0)
+ strCatFmt(resultStr, "\n");
+
+ // Stanza name and status
+ strCatFmt(
+ resultStr, "stanza: %s\n status: ", strPtr(varStr(kvGet(stanzaInfo, varNewStr(STANZA_KEY_NAME_STR)))));
+
+ // If an error has occurred, provide the information that is available and move onto next stanza
+ KeyValue *stanzaStatus = varKv(kvGet(stanzaInfo, varNewStr(STANZA_KEY_STATUS_STR)));
+ int statusCode = varInt(kvGet(stanzaStatus, varNewStr(STATUS_KEY_CODE_STR)));
+
+ if (statusCode != INFO_STANZA_STATUS_CODE_OK)
+ {
+ strCatFmt(
+ resultStr, "%s (%s)\n", strPtr(INFO_STANZA_STATUS_ERROR),
+ strPtr(varStr(kvGet(stanzaStatus, varNewStr(STATUS_KEY_MESSAGE_STR)))));
+
+ if (statusCode == INFO_STANZA_STATUS_CODE_MISSING_STANZA_DATA ||
+ statusCode == INFO_STANZA_STATUS_CODE_NO_BACKUP)
+ {
+ strCatFmt(
+ resultStr, " cipher: %s\n", strPtr(varStr(kvGet(stanzaInfo, varNewStr(STANZA_KEY_CIPHER_STR)))));
+
+ // If there is a backup.info file but no backups, then process the archive info
+ if (statusCode == INFO_STANZA_STATUS_CODE_NO_BACKUP)
+ formatTextDb(stanzaInfo, resultStr);
+ }
+
+ continue;
+ }
+ else
+ strCatFmt(resultStr, "%s\n", strPtr(INFO_STANZA_STATUS_OK));
+
+ // Cipher
+ strCatFmt(resultStr, " cipher: %s\n",
+ strPtr(varStr(kvGet(stanzaInfo, varNewStr(STANZA_KEY_CIPHER_STR)))));
+
+ formatTextDb(stanzaInfo, resultStr);
+ }
+ }
+ else
+ resultStr = strNewFmt("No stanzas exist in %s\n", strPtr(storagePathNP(storageRepo(), NULL)));
+ }
+ // Format json output
+ else
+ resultStr = varToJson(varNewVarLst(infoList), 4);
+
+ memContextSwitch(MEM_CONTEXT_OLD());
+ result = strDup(resultStr);
+ memContextSwitch(MEM_CONTEXT_TEMP());
+ }
+ MEM_CONTEXT_TEMP_END();
+
+ FUNCTION_DEBUG_RESULT(STRING, result);
+}
+
+/***********************************************************************************************************************************
+Render info and output to stdout
+***********************************************************************************************************************************/
+void
+cmdInfo(void)
+{
+ FUNCTION_DEBUG_VOID(logLevelDebug);
+
+ MEM_CONTEXT_TEMP_BEGIN()
+ {
+ if (!cfgOptionTest(cfgOptRepoHost)) // {uncovered - Perl code is covered in integration tests}
+ {
+ ioHandleWriteOneStr(STDOUT_FILENO, infoRender());
+ }
+ // Else do it in Perl
+ else
+ perlExec(); // {+uncovered}
+ }
+ MEM_CONTEXT_TEMP_END();
+
+ FUNCTION_DEBUG_RESULT_VOID();
+}
diff --git a/src/command/info/info.h b/src/command/info/info.h
new file mode 100644
index 000000000..0ab728a59
--- /dev/null
+++ b/src/command/info/info.h
@@ -0,0 +1,12 @@
+/***********************************************************************************************************************************
+Info Command
+***********************************************************************************************************************************/
+#ifndef COMMAND_INFO_INFO_H
+#define COMMAND_INFO_INFO_H
+
+/***********************************************************************************************************************************
+Functions
+***********************************************************************************************************************************/
+void cmdInfo(void);
+
+#endif
diff --git a/test/define.yaml b/test/define.yaml
index ca7e2d5a6..7e3e7eccb 100644
--- a/test/define.yaml
+++ b/test/define.yaml
@@ -612,6 +612,13 @@ unit:
coverage:
command/control/control: full
+ # ----------------------------------------------------------------------------------------------------------------------------
+ - name: info
+ total: 3
+
+ coverage:
+ command/info/info: full
+
# ********************************************************************************************************************************
- name: archive
diff --git a/test/src/module/command/infoTest.c b/test/src/module/command/infoTest.c
new file mode 100644
index 000000000..a9f96a0ec
--- /dev/null
+++ b/test/src/module/command/infoTest.c
@@ -0,0 +1,853 @@
+/***********************************************************************************************************************************
+Test Info Command
+***********************************************************************************************************************************/
+#include "storage/driver/posix/storage.h"
+
+#include "common/harnessConfig.h"
+
+/***********************************************************************************************************************************
+Test Run
+***********************************************************************************************************************************/
+void
+testRun(void)
+{
+ FUNCTION_HARNESS_VOID();
+
+ // Create the repo directories
+ String *repoPath = strNewFmt("%s/repo", testPath());
+ String *archivePath = strNewFmt("%s/%s", strPtr(repoPath), "archive");
+ String *backupPath = strNewFmt("%s/%s", strPtr(repoPath), "backup");
+ String *archiveStanza1Path = strNewFmt("%s/stanza1", strPtr(archivePath));
+ String *backupStanza1Path = strNewFmt("%s/stanza1", strPtr(backupPath));
+
+ // *****************************************************************************************************************************
+ if (testBegin("infoRender()"))
+ {
+ StringList *argList = strLstNew();
+ strLstAddZ(argList, "pgbackrest");
+ strLstAdd(argList, strNewFmt("--repo-path=%s", strPtr(repoPath)));
+ strLstAddZ(argList, "info");
+ StringList *argListText = strLstDup(argList);
+
+ strLstAddZ(argList, "--output=json");
+ harnessCfgLoad(strLstSize(argList), strLstPtr(argList));
+
+ // No repo path
+ //--------------------------------------------------------------------------------------------------------------------------
+ TEST_ERROR_FMT(
+ infoRender(), PathOpenError,
+ "unable to open path '%s' for read: [2] No such file or directory", strPtr(backupPath));
+
+ storagePathCreateNP(storageLocalWrite(), archivePath);
+ storagePathCreateNP(storageLocalWrite(), backupPath);
+
+ // No stanzas have been created
+ //--------------------------------------------------------------------------------------------------------------------------
+ TEST_RESULT_STR(strPtr(infoRender()), "[]\n", "json - repo but no stanzas");
+
+ harnessCfgLoad(strLstSize(argListText), strLstPtr(argListText));
+ TEST_RESULT_STR(strPtr(infoRender()),
+ strPtr(strNewFmt("No stanzas exist in %s\n", strPtr(storagePathNP(storageRepo(), NULL)))), "text - no stanzas");
+
+ // Empty stanza
+ //--------------------------------------------------------------------------------------------------------------------------
+ TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), backupStanza1Path), "backup stanza1 directory");
+ TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), archiveStanza1Path), "archive stanza1 directory");
+ TEST_RESULT_STR(strPtr(infoRender()),
+ "stanza: stanza1\n"
+ " status: error (missing stanza data)\n"
+ " cipher: none\n", "text - missing stanza data");
+
+ harnessCfgLoad(strLstSize(argList), strLstPtr(argList));
+ TEST_RESULT_STR(strPtr(infoRender()),
+ "[\n"
+ " {\n"
+ " \"archive\" : [],\n"
+ " \"backup\" : [],\n"
+ " \"cipher\" : \"none\",\n"
+ " \"db\" : [],\n"
+ " \"name\" : \"stanza1\",\n"
+ " \"status\" : {\n"
+ " \"code\" : 3,\n"
+ " \"message\" : \"missing stanza data\"\n"
+ " }\n"
+ " }\n"
+ "]\n", "json - missing stanza data");
+
+ // backup.info file exists, but archive.info does not
+ //--------------------------------------------------------------------------------------------------------------------------
+ String *content = strNew
+ (
+ "[backrest]\n"
+ "backrest-checksum=\"51774ffab293c5cfb07511d7d2e101e92416f4ed\"\n"
+ "backrest-format=5\n"
+ "backrest-version=\"2.04\"\n"
+ "\n"
+ "[db]\n"
+ "db-catalog-version=201409291\n"
+ "db-control-version=942\n"
+ "db-id=2\n"
+ "db-system-id=6569239123849665679\n"
+ "db-version=\"9.4\"\n"
+ "\n"
+ "[db:history]\n"
+ "1={\"db-catalog-version\":201306121,\"db-control-version\":937,\"db-system-id\":6569239123849665666,"
+ "\"db-version\":\"9.3\"}\n"
+ "2={\"db-catalog-version\":201409291,\"db-control-version\":942,\"db-system-id\":6569239123849665679,"
+ "\"db-version\":\"9.4\"}\n"
+ );
+
+ TEST_RESULT_VOID(
+ storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/backup.info", strPtr(backupStanza1Path))),
+ bufNewStr(content)), "put backup info to file");
+
+ TEST_ERROR_FMT(infoRender(), FileMissingError,
+ "unable to load info file '%s/archive.info' or '%s/archive.info.copy':\n"
+ "FileMissingError: unable to open '%s/archive.info' for read: [2] No such file or directory\n"
+ "FileMissingError: unable to open '%s/archive.info.copy' for read: [2] No such file or directory\n"
+ "HINT: archive.info cannot be opened but is required to push/get WAL segments.\n"
+ "HINT: is archive_command configured correctly in postgresql.conf?\n"
+ "HINT: has a stanza-create been performed?\n"
+ "HINT: use --no-archive-check to disable archive checks during backup if you have an alternate archiving scheme.",
+ strPtr(archiveStanza1Path), strPtr(archiveStanza1Path), strPtr(archiveStanza1Path), strPtr(archiveStanza1Path));
+
+ // backup.info/archive.info files exist, mismatched db ids, no backup:current section so no valid backups
+ // Only the current db information from the db:history will be processed.
+ //--------------------------------------------------------------------------------------------------------------------------
+ content = strNew
+ (
+ "[backrest]\n"
+ "backrest-checksum=\"0da11608456bae64c42cc1dc8df4ae79b953d597\"\n"
+ "backrest-format=5\n"
+ "backrest-version=\"2.04\"\n"
+ "\n"
+ "[db]\n"
+ "db-id=1\n"
+ "db-system-id=6569239123849665679\n"
+ "db-version=\"9.4\"\n"
+ "\n"
+ "[db:history]\n"
+ "1={\"db-id\":6569239123849665679,\"db-version\":\"9.4\"}\n"
+ "2={\"db-id\":6569239123849665666,\"db-version\":\"9.3\"}\n"
+ "3={\"db-id\":6569239123849665679,\"db-version\":\"9.4\"}\n"
+ );
+
+ TEST_RESULT_VOID(
+ storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/archive.info", strPtr(archiveStanza1Path))),
+ bufNewStr(content)), "put archive info to file");
+
+ // archive section will cross reference backup db-id 2 to archive db-id 3 but db section will only use the db-ids from
+ // backup.info
+ TEST_RESULT_STR(strPtr(infoRender()),
+ "[\n"
+ " {\n"
+ " \"archive\" : [\n"
+ " {\n"
+ " \"database\" : {\n"
+ " \"id\" : 2\n"
+ " },\n"
+ " \"id\" : \"9.4-3\",\n"
+ " \"max\" : null,\n"
+ " \"min\" : null\n"
+ " }\n"
+ " ],\n"
+ " \"backup\" : [],\n"
+ " \"cipher\" : \"none\",\n"
+ " \"db\" : [\n"
+ " {\n"
+ " \"id\" : 2,\n"
+ " \"system-id\" : 6569239123849665679,\n"
+ " \"version\" : \"9.4\"\n"
+ " },\n"
+ " {\n"
+ " \"id\" : 1,\n"
+ " \"system-id\" : 6569239123849665666,\n"
+ " \"version\" : \"9.3\"\n"
+ " }\n"
+ " ],\n"
+ " \"name\" : \"stanza1\",\n"
+ " \"status\" : {\n"
+ " \"code\" : 2,\n"
+ " \"message\" : \"no valid backups\"\n"
+ " }\n"
+ " }\n"
+ "]\n", "json - single stanza, no valid backups");
+
+ harnessCfgLoad(strLstSize(argListText), strLstPtr(argListText));
+ TEST_RESULT_STR(strPtr(infoRender()),
+ "stanza: stanza1\n"
+ " status: error (no valid backups)\n"
+ " cipher: none\n"
+ "\n"
+ " db (current)\n"
+ " wal archive min/max (9.4-3): none present\n",
+ "text - single stanza, no valid backups");
+
+ // Coverage for stanzaStatus branches
+ //--------------------------------------------------------------------------------------------------------------------------
+ String *archiveDb1_1 = strNewFmt("%s/9.4-1/0000000100000000", strPtr(archiveStanza1Path));
+ TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), archiveDb1_1), "create db1 archive WAL1 directory");
+ TEST_RESULT_INT(system(
+ strPtr(strNewFmt("touch %s", strPtr(strNewFmt("%s/000000010000000000000002-ac61b8f1ec7b1e6c3eaee9345214595eb7daa9a1.gz",
+ strPtr(archiveDb1_1)))))), 0, "touch WAL1 file");
+ TEST_RESULT_INT(system(
+ strPtr(strNewFmt("touch %s", strPtr(strNewFmt("%s/000000010000000000000003-37dff2b7552a9d66e4bae1a762488a6885e7082c.gz",
+ strPtr(archiveDb1_1)))))), 0, "touch WAL1 file");
+
+ String *archiveDb1_2 = strNewFmt("%s/9.4-1/0000000200000000", strPtr(archiveStanza1Path));
+ TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), archiveDb1_2), "create db1 archive WAL2 directory");
+ TEST_RESULT_INT(system(
+ strPtr(strNewFmt("touch %s", strPtr(strNewFmt("%s/000000020000000000000003-37dff2b7552a9d66e4bae1a762488a6885e7082c.gz",
+ strPtr(archiveDb1_2)))))), 0, "touch WAL2 file");
+
+ String *archiveDb1_3 = strNewFmt("%s/9.4-1/0000000300000000", strPtr(archiveStanza1Path));
+ TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), archiveDb1_3), "create db1 archive WAL3 directory");
+
+ String *archiveDb3 = strNewFmt("%s/9.4-3/0000000100000000", strPtr(archiveStanza1Path));
+ TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), archiveDb3), "create db3 archive WAL1 directory");
+
+ harnessCfgLoad(strLstSize(argList), strLstPtr(argList));
+ content = strNew
+ (
+ "[backrest]\n"
+ "backrest-checksum=\"2edbac400200cc0bd559951f1ee166de5c6f5f49\"\n"
+ "backrest-format=5\n"
+ "backrest-version=\"2.04\"\n"
+ "\n"
+ "[db]\n"
+ "db-catalog-version=201409291\n"
+ "db-control-version=942\n"
+ "db-id=3\n"
+ "db-system-id=6569239123849665679\n"
+ "db-version=\"9.4\"\n"
+ "\n"
+ "[backup:current]\n"
+ "20181116-154756F={\"backrest-format\":5,\"backrest-version\":\"2.04\","
+ "\"backup-archive-start\":null,\"backup-archive-stop\":null,"
+ "\"backup-info-repo-size\":3159776,\"backup-info-repo-size-delta\":3159,\"backup-info-size\":26897030,"
+ "\"backup-info-size-delta\":26897030,\"backup-timestamp-start\":1542383276,\"backup-timestamp-stop\":1542383289,"
+ "\"backup-type\":\"full\",\"db-id\":1,\"option-archive-check\":true,\"option-archive-copy\":false,"
+ "\"option-backup-standby\":false,\"option-checksum-page\":true,\"option-compress\":true,\"option-hardlink\":false,"
+ "\"option-online\":true}\n"
+ "\n"
+ "[db:history]\n"
+ "1={\"db-catalog-version\":201409291,\"db-control-version\":942,\"db-system-id\":6569239123849665679,"
+ "\"db-version\":\"9.4\"}\n"
+ "2={\"db-catalog-version\":201306121,\"db-control-version\":937,\"db-system-id\":6569239123849665666,"
+ "\"db-version\":\"9.3\"}\n"
+ "3={\"db-catalog-version\":201409291,\"db-control-version\":942,\"db-system-id\":6569239123849665679,"
+ "\"db-version\":\"9.4\"}\n"
+ );
+
+ TEST_RESULT_VOID(
+ storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/backup.info", strPtr(backupStanza1Path))),
+ bufNewStr(content)), "put backup info to file");
+
+ TEST_RESULT_STR(strPtr(infoRender()),
+ "[\n"
+ " {\n"
+ " \"archive\" : [\n"
+ " {\n"
+ " \"database\" : {\n"
+ " \"id\" : 3\n"
+ " },\n"
+ " \"id\" : \"9.4-3\",\n"
+ " \"max\" : null,\n"
+ " \"min\" : null\n"
+ " },\n"
+ " {\n"
+ " \"database\" : {\n"
+ " \"id\" : 1\n"
+ " },\n"
+ " \"id\" : \"9.4-1\",\n"
+ " \"max\" : \"000000020000000000000003\",\n"
+ " \"min\" : \"000000010000000000000002\"\n"
+ " }\n"
+ " ],\n"
+ " \"backup\" : [\n"
+ " {\n"
+ " \"archive\" : {\n"
+ " \"start\" : null,\n"
+ " \"stop\" : null\n"
+ " },\n"
+ " \"backrest\" : {\n"
+ " \"format\" : 5,\n"
+ " \"version\" : \"2.04\"\n"
+ " },\n"
+ " \"database\" : {\n"
+ " \"id\" : 1\n"
+ " },\n"
+ " \"info\" : {\n"
+ " \"delta\" : 26897030,\n"
+ " \"repository\" : {\n"
+ " \"delta\" : 3159,\n"
+ " \"size\" : 3159776\n"
+ " },\n"
+ " \"size\" : 26897030\n"
+ " },\n"
+ " \"label\" : \"20181116-154756F\",\n"
+ " \"prior\" : null,\n"
+ " \"reference\" : null,\n"
+ " \"timestamp\" : {\n"
+ " \"start\" : 1542383276,\n"
+ " \"stop\" : 1542383289\n"
+ " },\n"
+ " \"type\" : \"full\"\n"
+ " }\n"
+ " ],\n"
+ " \"cipher\" : \"none\",\n"
+ " \"db\" : [\n"
+ " {\n"
+ " \"id\" : 3,\n"
+ " \"system-id\" : 6569239123849665679,\n"
+ " \"version\" : \"9.4\"\n"
+ " },\n"
+ " {\n"
+ " \"id\" : 2,\n"
+ " \"system-id\" : 6569239123849665666,\n"
+ " \"version\" : \"9.3\"\n"
+ " },\n"
+ " {\n"
+ " \"id\" : 1,\n"
+ " \"system-id\" : 6569239123849665679,\n"
+ " \"version\" : \"9.4\"\n"
+ " }\n"
+ " ],\n"
+ " \"name\" : \"stanza1\",\n"
+ " \"status\" : {\n"
+ " \"code\" : 0,\n"
+ " \"message\" : \"ok\"\n"
+ " }\n"
+ " }\n"
+ "]\n", "json - single stanza, valid backup, no priors, no archives in latest DB");
+
+ harnessCfgLoad(strLstSize(argListText), strLstPtr(argListText));
+ TEST_RESULT_STR(strPtr(infoRender()),
+ "stanza: stanza1\n"
+ " status: ok\n"
+ " cipher: none\n"
+ "\n"
+ " db (current)\n"
+ " wal archive min/max (9.4-3): none present\n"
+ "\n"
+ " db (prior)\n"
+ " wal archive min/max (9.4-1): 000000010000000000000002/000000020000000000000003\n"
+ "\n"
+ " full backup: 20181116-154756F\n"
+ " timestamp start/stop: 2018-11-16 15:47:56 / 2018-11-16 15:48:09\n"
+ " wal start/stop: n/a\n"
+ " database size: 25.7MB, backup size: 25.7MB\n"
+ " repository size: 3MB, repository backup size: 3KB\n"
+ ,"text - single stanza, valid backup, no priors, no archives in latest DB");
+
+ // backup.info/archive.info files exist, backups exist, archives exist
+ //--------------------------------------------------------------------------------------------------------------------------
+ content = strNew
+ (
+ "[backrest]\n"
+ "backrest-checksum=\"075a202d42c3b6a0257da5f73a68fa77b342f777\"\n"
+ "backrest-format=5\n"
+ "backrest-version=\"2.08dev\"\n"
+ "\n"
+ "[db]\n"
+ "db-id=2\n"
+ "db-system-id=6626363367545678089\n"
+ "db-version=\"9.5\"\n"
+ "\n"
+ "[db:history]\n"
+ "1={\"db-id\":6625592122879095702,\"db-version\":\"9.4\"}\n"
+ "2={\"db-id\":6626363367545678089,\"db-version\":\"9.5\"}\n"
+ );
+
+ TEST_RESULT_VOID(
+ storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/archive.info", strPtr(archiveStanza1Path))),
+ bufNewStr(content)), "put archive info to file - stanza1");
+
+ content = strNew
+ (
+ "[backrest]\n"
+ "backrest-checksum=\"b50db7cf8f659ac15a0c7a2f45a0813f46a68c6b\"\n"
+ "backrest-format=5\n"
+ "backrest-version=\"2.08dev\"\n"
+ "\n"
+ "[backup:current]\n"
+ "20181119-152138F={"
+ "\"backrest-format\":5,\"backrest-version\":\"2.08dev\","
+ "\"backup-archive-start\":\"000000010000000000000002\",\"backup-archive-stop\":\"000000010000000000000002\","
+ "\"backup-info-repo-size\":2369186,\"backup-info-repo-size-delta\":2369186,"
+ "\"backup-info-size\":20162900,\"backup-info-size-delta\":20162900,"
+ "\"backup-timestamp-start\":1542640898,\"backup-timestamp-stop\":1542640911,\"backup-type\":\"full\","
+ "\"db-id\":1,\"option-archive-check\":true,\"option-archive-copy\":false,\"option-backup-standby\":false,"
+ "\"option-checksum-page\":true,\"option-compress\":true,\"option-hardlink\":false,\"option-online\":true}\n"
+ "20181119-152138F_20181119-152152D={"
+ "\"backrest-format\":5,\"backrest-version\":\"2.08dev\",\"backup-archive-start\":\"000000010000000000000003\","
+ "\"backup-archive-stop\":\"000000010000000000000003\",\"backup-info-repo-size\":2369186,"
+ "\"backup-info-repo-size-delta\":346,\"backup-info-size\":20162900,\"backup-info-size-delta\":8428,"
+ "\"backup-prior\":\"20181119-152138F\",\"backup-reference\":[\"20181119-152138F\"],"
+ "\"backup-timestamp-start\":1542640912,\"backup-timestamp-stop\":1542640915,\"backup-type\":\"diff\","
+ "\"db-id\":1,\"option-archive-check\":true,\"option-archive-copy\":false,\"option-backup-standby\":false,"
+ "\"option-checksum-page\":true,\"option-compress\":true,\"option-hardlink\":false,\"option-online\":true}\n"
+ "20181119-152138F_20181119-152152I={"
+ "\"backrest-format\":5,\"backrest-version\":\"2.08dev\",\"backup-archive-start\":\"000000010000000000000003\","
+ "\"backup-info-repo-size\":2369186,"
+ "\"backup-info-repo-size-delta\":346,\"backup-info-size\":20162900,\"backup-info-size-delta\":8428,"
+ "\"backup-prior\":\"20181119-152138F_20181119-152152D\","
+ "\"backup-reference\":[\"20181119-152138F\",\"20181119-152138F_20181119-152152D\"],"
+ "\"backup-timestamp-start\":1542640912,\"backup-timestamp-stop\":1542640915,\"backup-type\":\"incr\","
+ "\"db-id\":1,\"option-archive-check\":true,\"option-archive-copy\":false,\"option-backup-standby\":false,"
+ "\"option-checksum-page\":true,\"option-compress\":true,\"option-hardlink\":false,\"option-online\":true}\n"
+ "\n"
+ "[db]\n"
+ "db-catalog-version=201510051\n"
+ "db-control-version=942\n"
+ "db-id=2\n"
+ "db-system-id=6626363367545678089\n"
+ "db-version=\"9.5\"\n"
+ "\n"
+ "[db:history]\n"
+ "1={\"db-catalog-version\":201409291,\"db-control-version\":942,\"db-system-id\":6625592122879095702,"
+ "\"db-version\":\"9.4\"}\n"
+ "2={\"db-catalog-version\":201510051,\"db-control-version\":942,\"db-system-id\":6626363367545678089,"
+ "\"db-version\":\"9.5\"}\n"
+ );
+
+ TEST_RESULT_VOID(
+ storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/backup.info", strPtr(backupStanza1Path))),
+ bufNewStr(content)), "put backup info to file - stanza1");
+
+ String *archiveStanza2Path = strNewFmt("%s/stanza2", strPtr(archivePath));
+ String *backupStanza2Path = strNewFmt("%s/stanza2", strPtr(backupPath));
+ TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), backupStanza1Path), "backup stanza2 directory");
+ TEST_RESULT_VOID(storagePathCreateNP(storageLocalWrite(), archiveStanza1Path), "archive stanza2 directory");
+
+ content = strNew
+ (
+ "[backrest]\n"
+ "backrest-checksum=\"6779d476833114925a73e058ef9ff04e5a8c7bd2\"\n"
+ "backrest-format=5\n"
+ "backrest-version=\"2.08dev\"\n"
+ "\n"
+ "[db]\n"
+ "db-id=1\n"
+ "db-system-id=6625633699176220261\n"
+ "db-version=\"9.4\"\n"
+ "\n"
+ "[db:history]\n"
+ "1={\"db-id\":6625633699176220261,\"db-version\":\"9.4\"}\n"
+ );
+
+ TEST_RESULT_VOID(
+ storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/archive.info", strPtr(archiveStanza2Path))),
+ bufNewStr(content)), "put archive info to file - stanza2");
+
+ content = strNew
+ (
+ "[backrest]\n"
+ "backrest-checksum=\"2393c52cb48aff2d6c6e87e21a34a3e28200f42e\"\n"
+ "backrest-format=5\n"
+ "backrest-version=\"2.08dev\"\n"
+ "\n"
+ "[db]\n"
+ "db-catalog-version=201409291\n"
+ "db-control-version=942\n"
+ "db-id=1\n"
+ "db-system-id=6625633699176220261\n"
+ "db-version=\"9.4\"\n"
+ "\n"
+ "[db:history]\n"
+ "1={\"db-catalog-version\":201409291,\"db-control-version\":942,\"db-system-id\":6625633699176220261,"
+ "\"db-version\":\"9.4\"}\n"
+ );
+
+ TEST_RESULT_VOID(
+ storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/backup.info", strPtr(backupStanza2Path))),
+ bufNewStr(content)), "put backup info to file - stanza2");
+
+ harnessCfgLoad(strLstSize(argList), strLstPtr(argList));
+ TEST_RESULT_STR(strPtr(infoRender()),
+ "[\n"
+ " {\n"
+ " \"archive\" : [\n"
+ " {\n"
+ " \"database\" : {\n"
+ " \"id\" : 2\n"
+ " },\n"
+ " \"id\" : \"9.5-2\",\n"
+ " \"max\" : null,\n"
+ " \"min\" : null\n"
+ " },\n"
+ " {\n"
+ " \"database\" : {\n"
+ " \"id\" : 1\n"
+ " },\n"
+ " \"id\" : \"9.4-1\",\n"
+ " \"max\" : \"000000020000000000000003\",\n"
+ " \"min\" : \"000000010000000000000002\"\n"
+ " }\n"
+ " ],\n"
+ " \"backup\" : [\n"
+ " {\n"
+ " \"archive\" : {\n"
+ " \"start\" : \"000000010000000000000002\",\n"
+ " \"stop\" : \"000000010000000000000002\"\n"
+ " },\n"
+ " \"backrest\" : {\n"
+ " \"format\" : 5,\n"
+ " \"version\" : \"2.08dev\"\n"
+ " },\n"
+ " \"database\" : {\n"
+ " \"id\" : 1\n"
+ " },\n"
+ " \"info\" : {\n"
+ " \"delta\" : 20162900,\n"
+ " \"repository\" : {\n"
+ " \"delta\" : 2369186,\n"
+ " \"size\" : 2369186\n"
+ " },\n"
+ " \"size\" : 20162900\n"
+ " },\n"
+ " \"label\" : \"20181119-152138F\",\n"
+ " \"prior\" : null,\n"
+ " \"reference\" : null,\n"
+ " \"timestamp\" : {\n"
+ " \"start\" : 1542640898,\n"
+ " \"stop\" : 1542640911\n"
+ " },\n"
+ " \"type\" : \"full\"\n"
+ " },\n"
+ " {\n"
+ " \"archive\" : {\n"
+ " \"start\" : \"000000010000000000000003\",\n"
+ " \"stop\" : \"000000010000000000000003\"\n"
+ " },\n"
+ " \"backrest\" : {\n"
+ " \"format\" : 5,\n"
+ " \"version\" : \"2.08dev\"\n"
+ " },\n"
+ " \"database\" : {\n"
+ " \"id\" : 1\n"
+ " },\n"
+ " \"info\" : {\n"
+ " \"delta\" : 8428,\n"
+ " \"repository\" : {\n"
+ " \"delta\" : 346,\n"
+ " \"size\" : 2369186\n"
+ " },\n"
+ " \"size\" : 20162900\n"
+ " },\n"
+ " \"label\" : \"20181119-152138F_20181119-152152D\",\n"
+ " \"prior\" : \"20181119-152138F\",\n"
+ " \"reference\" : [\n"
+ " \"20181119-152138F\"\n"
+ " ],\n"
+ " \"timestamp\" : {\n"
+ " \"start\" : 1542640912,\n"
+ " \"stop\" : 1542640915\n"
+ " },\n"
+ " \"type\" : \"diff\"\n"
+ " },\n"
+ " {\n"
+ " \"archive\" : {\n"
+ " \"start\" : \"000000010000000000000003\",\n"
+ " \"stop\" : null\n"
+ " },\n"
+ " \"backrest\" : {\n"
+ " \"format\" : 5,\n"
+ " \"version\" : \"2.08dev\"\n"
+ " },\n"
+ " \"database\" : {\n"
+ " \"id\" : 1\n"
+ " },\n"
+ " \"info\" : {\n"
+ " \"delta\" : 8428,\n"
+ " \"repository\" : {\n"
+ " \"delta\" : 346,\n"
+ " \"size\" : 2369186\n"
+ " },\n"
+ " \"size\" : 20162900\n"
+ " },\n"
+ " \"label\" : \"20181119-152138F_20181119-152152I\",\n"
+ " \"prior\" : \"20181119-152138F_20181119-152152D\",\n"
+ " \"reference\" : [\n"
+ " \"20181119-152138F\",\n"
+ " \"20181119-152138F_20181119-152152D\"\n"
+ " ],\n"
+ " \"timestamp\" : {\n"
+ " \"start\" : 1542640912,\n"
+ " \"stop\" : 1542640915\n"
+ " },\n"
+ " \"type\" : \"incr\"\n"
+ " }\n"
+ " ],\n"
+ " \"cipher\" : \"none\",\n"
+ " \"db\" : [\n"
+ " {\n"
+ " \"id\" : 2,\n"
+ " \"system-id\" : 6626363367545678089,\n"
+ " \"version\" : \"9.5\"\n"
+ " },\n"
+ " {\n"
+ " \"id\" : 1,\n"
+ " \"system-id\" : 6625592122879095702,\n"
+ " \"version\" : \"9.4\"\n"
+ " }\n"
+ " ],\n"
+ " \"name\" : \"stanza1\",\n"
+ " \"status\" : {\n"
+ " \"code\" : 0,\n"
+ " \"message\" : \"ok\"\n"
+ " }\n"
+ " },\n"
+ " {\n"
+ " \"archive\" : [\n"
+ " {\n"
+ " \"database\" : {\n"
+ " \"id\" : 1\n"
+ " },\n"
+ " \"id\" : \"9.4-1\",\n"
+ " \"max\" : null,\n"
+ " \"min\" : null\n"
+ " }\n"
+ " ],\n"
+ " \"backup\" : [],\n"
+ " \"cipher\" : \"none\",\n"
+ " \"db\" : [\n"
+ " {\n"
+ " \"id\" : 1,\n"
+ " \"system-id\" : 6625633699176220261,\n"
+ " \"version\" : \"9.4\"\n"
+ " }\n"
+ " ],\n"
+ " \"name\" : \"stanza2\",\n"
+ " \"status\" : {\n"
+ " \"code\" : 2,\n"
+ " \"message\" : \"no valid backups\"\n"
+ " }\n"
+ " }\n"
+ "]\n", "json - multiple stanzas, one with valid backups, archives in latest DB");
+
+ harnessCfgLoad(strLstSize(argListText), strLstPtr(argListText));
+ TEST_RESULT_STR(strPtr(infoRender()),
+ "stanza: stanza1\n"
+ " status: ok\n"
+ " cipher: none\n"
+ "\n"
+ " db (current)\n"
+ " wal archive min/max (9.5-2): none present\n"
+ "\n"
+ " db (prior)\n"
+ " wal archive min/max (9.4-1): 000000010000000000000002/000000020000000000000003\n"
+ "\n"
+ " full backup: 20181119-152138F\n"
+ " timestamp start/stop: 2018-11-19 15:21:38 / 2018-11-19 15:21:51\n"
+ " wal start/stop: 000000010000000000000002 / 000000010000000000000002\n"
+ " database size: 19.2MB, backup size: 19.2MB\n"
+ " repository size: 2.3MB, repository backup size: 2.3MB\n"
+ "\n"
+ " diff backup: 20181119-152138F_20181119-152152D\n"
+ " timestamp start/stop: 2018-11-19 15:21:52 / 2018-11-19 15:21:55\n"
+ " wal start/stop: 000000010000000000000003 / 000000010000000000000003\n"
+ " database size: 19.2MB, backup size: 8.2KB\n"
+ " repository size: 2.3MB, repository backup size: 346B\n"
+ " backup reference list: 20181119-152138F\n"
+ "\n"
+ " incr backup: 20181119-152138F_20181119-152152I\n"
+ " timestamp start/stop: 2018-11-19 15:21:52 / 2018-11-19 15:21:55\n"
+ " wal start/stop: n/a\n"
+ " database size: 19.2MB, backup size: 8.2KB\n"
+ " repository size: 2.3MB, repository backup size: 346B\n"
+ " backup reference list: 20181119-152138F, 20181119-152138F_20181119-152152D\n"
+ "\n"
+ "stanza: stanza2\n"
+ " status: error (no valid backups)\n"
+ " cipher: none\n"
+ "\n"
+ " db (current)\n"
+ " wal archive min/max (9.4-1): none present\n"
+ , "text - multiple stanzas, one with valid backups, archives in latest DB");
+
+ // Stanza not found
+ //--------------------------------------------------------------------------------------------------------------------------
+ StringList *argList2 = strLstDup(argList);
+ strLstAddZ(argList2, "--stanza=silly");
+ harnessCfgLoad(strLstSize(argList2), strLstPtr(argList2));
+ TEST_RESULT_STR(strPtr(infoRender()),
+ "[\n"
+ " {\n"
+ " \"backup\" : [],\n"
+ " \"db\" : [],\n"
+ " \"name\" : \"silly\",\n"
+ " \"status\" : {\n"
+ " \"code\" : 1,\n"
+ " \"message\" : \"missing stanza path\"\n"
+ " }\n"
+ " }\n"
+ "]\n", "json - missing stanza path");
+
+ StringList *argListText2 = strLstDup(argListText);
+ strLstAddZ(argListText2, "--stanza=silly");
+ harnessCfgLoad(strLstSize(argListText2), strLstPtr(argListText2));
+ TEST_RESULT_STR(strPtr(infoRender()),
+ "stanza: silly\n"
+ " status: error (missing stanza path)\n"
+ , "text - missing stanza path");
+
+ // Stanza found
+ //--------------------------------------------------------------------------------------------------------------------------
+ strLstAddZ(argList, "--stanza=stanza2");
+ harnessCfgLoad(strLstSize(argList), strLstPtr(argList));
+ TEST_RESULT_STR(strPtr(infoRender()),
+ "[\n"
+ " {\n"
+ " \"archive\" : [\n"
+ " {\n"
+ " \"database\" : {\n"
+ " \"id\" : 1\n"
+ " },\n"
+ " \"id\" : \"9.4-1\",\n"
+ " \"max\" : null,\n"
+ " \"min\" : null\n"
+ " }\n"
+ " ],\n"
+ " \"backup\" : [],\n"
+ " \"cipher\" : \"none\",\n"
+ " \"db\" : [\n"
+ " {\n"
+ " \"id\" : 1,\n"
+ " \"system-id\" : 6625633699176220261,\n"
+ " \"version\" : \"9.4\"\n"
+ " }\n"
+ " ],\n"
+ " \"name\" : \"stanza2\",\n"
+ " \"status\" : {\n"
+ " \"code\" : 2,\n"
+ " \"message\" : \"no valid backups\"\n"
+ " }\n"
+ " }\n"
+ "]\n"
+ , "json - multiple stanzas - selected found");
+
+ strLstAddZ(argListText, "--stanza=stanza2");
+ harnessCfgLoad(strLstSize(argListText), strLstPtr(argListText));
+ TEST_RESULT_STR(strPtr(infoRender()),
+ "stanza: stanza2\n"
+ " status: error (no valid backups)\n"
+ " cipher: none\n"
+ "\n"
+ " db (current)\n"
+ " wal archive min/max (9.4-1): none present\n"
+ ,"text - multiple stanzas - selected found");
+
+ // Crypto error
+ //--------------------------------------------------------------------------------------------------------------------------
+ content = strNew
+ (
+ "[global]\n"
+ "repo-cipher-pass=123abc\n"
+ );
+
+ TEST_RESULT_VOID(
+ storagePutNP(storageNewWriteNP(storageLocalWrite(), strNewFmt("%s/pgbackrest.conf", testPath())),
+ bufNewStr(content)), "put pgbackrest.conf file");
+ strLstAddZ(argListText, "--repo-cipher-type=aes-256-cbc");
+ strLstAdd(argListText, strNewFmt("--config=%s/pgbackrest.conf", testPath()));
+ harnessCfgLoad(strLstSize(argListText), strLstPtr(argListText));
+ TEST_ERROR_FMT(
+ infoRender(), CryptoError,
+ "unable to load info file '%s/backup.info' or '%s/backup.info.copy':\n"
+ "CryptoError: '%s/backup.info' cipher header invalid\n"
+ "HINT: Is or was the repo encrypted?\n"
+ "FileMissingError: unable to open '%s/backup.info.copy' for read: [2] No such file or directory\n"
+ "HINT: backup.info cannot be opened and is required to perform a backup.\n"
+ "HINT: has a stanza-create been performed?\n"
+ "HINT: use option --stanza if encryption settings are different for the stanza than the global settings"
+ ,strPtr(backupStanza2Path), strPtr(backupStanza2Path), strPtr(backupStanza2Path), strPtr(backupStanza2Path));
+ }
+
+ //******************************************************************************************************************************
+ if (testBegin("formatTextDb()"))
+ {
+ // These tests cover branches not covered in other tests
+ KeyValue *stanzaInfo = kvNew();
+ VariantList *dbSection = varLstNew();
+ Variant *pgInfo = varNewKv();
+ kvPut(varKv(pgInfo), varNewStr(DB_KEY_ID_STR), varNewUInt64(1));
+ kvPut(varKv(pgInfo), varNewStr(DB_KEY_SYSTEM_ID_STR), varNewUInt64(6625633699176220261));
+ kvPut(varKv(pgInfo), varNewStr(DB_KEY_VERSION_STR), varNewStr(pgVersionToStr(90500)));
+
+ varLstAdd(dbSection, pgInfo);
+
+ // Add the database history, backup and archive sections to the stanza info
+ kvPut(stanzaInfo, varNewStr(STANZA_KEY_DB_STR), varNewVarLst(dbSection));
+
+ VariantList *backupSection = varLstNew();
+ Variant *backupInfo = varNewKv();
+
+ kvPut(varKv(backupInfo), varNewStr(BACKUP_KEY_LABEL_STR), varNewStr(strNew("20181119-152138F")));
+ kvPut(varKv(backupInfo), varNewStr(BACKUP_KEY_TYPE_STR), varNewStr(strNew("full")));
+ kvPutKv(varKv(backupInfo), varNewStr(KEY_ARCHIVE_STR));
+ KeyValue *infoInfo = kvPutKv(varKv(backupInfo), varNewStr(BACKUP_KEY_INFO_STR));
+ kvPut(infoInfo, varNewStr(KEY_SIZE_STR), varNewUInt64(0));
+ kvPut(infoInfo, varNewStr(KEY_DELTA_STR), varNewUInt64(0));
+ KeyValue *repoInfo = kvPutKv(infoInfo, varNewStr(INFO_KEY_REPOSITORY_STR));
+ kvAdd(repoInfo, varNewStr(KEY_SIZE_STR), varNewUInt64(0));
+ kvAdd(repoInfo, varNewStr(KEY_DELTA_STR), varNewUInt64(0));
+ KeyValue *databaseInfo = kvPutKv(varKv(backupInfo), varNewStr(KEY_DATABASE_STR));
+ kvAdd(databaseInfo, varNewStr(DB_KEY_ID_STR), varNewUInt64(1));
+ KeyValue *timeInfo = kvPutKv(varKv(backupInfo), varNewStr(BACKUP_KEY_TIMESTAMP_STR));
+ kvAdd(timeInfo, varNewStr(KEY_START_STR), varNewUInt64(1542383276));
+ kvAdd(timeInfo, varNewStr(KEY_STOP_STR), varNewUInt64(1542383289));
+
+ varLstAdd(backupSection, backupInfo);
+
+ kvPut(stanzaInfo, varNewStr(STANZA_KEY_BACKUP_STR), varNewVarLst(backupSection));
+ kvPut(stanzaInfo, varNewStr(KEY_ARCHIVE_STR), varNewVarLst(varLstNew()));
+
+ String *result = strNew("");
+ formatTextDb(stanzaInfo, result);
+
+ TEST_RESULT_STR(strPtr(result),
+ "\n"
+ " db (current)\n"
+ " full backup: 20181119-152138F\n"
+ " timestamp start/stop: 2018-11-16 15:47:56 / 2018-11-16 15:48:09\n"
+ " wal start/stop: n/a\n"
+ " database size: 0B, backup size: 0B\n"
+ " repository size: 0B, repository backup size: 0B\n"
+ ,"formatTextDb only backup section (code cverage only)");
+ }
+
+ //******************************************************************************************************************************
+ if (testBegin("cmdInfo()"))
+ {
+ StringList *argList = strLstNew();
+ strLstAddZ(argList, "pgbackrest");
+ strLstAdd(argList, strNewFmt("--repo-path=%s", strPtr(repoPath)));
+ strLstAddZ(argList, "info");
+ harnessCfgLoad(strLstSize(argList), strLstPtr(argList));
+
+ storagePathCreateNP(storageLocalWrite(), archivePath);
+ storagePathCreateNP(storageLocalWrite(), backupPath);
+
+ // Redirect stdout to a file
+ int stdoutSave = dup(STDOUT_FILENO);
+ String *stdoutFile = strNewFmt("%s/stdout.info", testPath());
+
+ if (freopen(strPtr(stdoutFile), "w", stdout) == NULL) // {uncoverable - does not fail}
+ THROW_SYS_ERROR(FileWriteError, "unable to reopen stdout"); // {uncoverable+}
+
+ // Not in a test wrapper to avoid writing to stdout
+ cmdInfo();
+
+ // Restore normal stdout
+ dup2(stdoutSave, STDOUT_FILENO);
+
+ const char *generalHelp = strPtr(strNewFmt("No stanzas exist in %s\n", strPtr(repoPath)));
+
+ Storage *storage = storageDriverPosixInterface(
+ storageDriverPosixNew(strNew(testPath()), STORAGE_MODE_FILE_DEFAULT, STORAGE_MODE_PATH_DEFAULT, false, NULL));
+ TEST_RESULT_STR(strPtr(strNewBuf(storageGetNP(storageNewReadNP(storage, stdoutFile)))), generalHelp, " check text");
+ }
+
+ FUNCTION_HARNESS_RESULT_VOID();
+}