You've already forked pgbackrest
mirror of
https://github.com/pgbackrest/pgbackrest.git
synced 2026-04-26 21:02:59 +02:00
Refactor restore module into included modules.
The restore module has been large and unwieldy for some time but I have been loathe to split it into separate compilation units because it means maintaining additional header files, updating build files, and losing optimizations from static functions. These functions are only used internally by restore and it seems wasteful to extern them. We already have a number of cases where C files are included directly into other C files, especially .vendor.c.inc and .auto.c.inc files. We also include C files to add functionality needed for build/doc/test to core objects without having to add that functionality to core. See src/build/common/string.c for an example. The test/coverage code already supports C includes but I had to update it to recognize the new extension.
This commit is contained in:
@@ -12,5 +12,19 @@
|
||||
<p>Suppress <id>unused parameter</id> errors in <proper>meson</proper> compiler probes.</p>
|
||||
</release-item>
|
||||
</release-improvement-list>
|
||||
|
||||
<release-development-list>
|
||||
<release-item>
|
||||
<github-issue id="2731"/>
|
||||
|
||||
<release-item-contributor-list>
|
||||
<release-item-contributor id="david.steele"/>
|
||||
<release-item-reviewer id="douglas.j.hunley"/>
|
||||
<release-item-reviewer id="david.christensen"/>
|
||||
</release-item-contributor-list>
|
||||
|
||||
<p>Refactor restore module into included modules.</p>
|
||||
</release-item>
|
||||
</release-development-list>
|
||||
</release-core-list>
|
||||
</release>
|
||||
|
||||
@@ -0,0 +1,640 @@
|
||||
/***********************************************************************************************************************************
|
||||
Check ownership of items in the manifest
|
||||
***********************************************************************************************************************************/
|
||||
// Helper to determine what the user/group of a path/file/link should be
|
||||
static const String *
|
||||
restoreManifestOwnerReplace(const String *const owner, const String *const ownerDefaultRoot)
|
||||
{
|
||||
FUNCTION_TEST_BEGIN();
|
||||
FUNCTION_TEST_PARAM(STRING, owner);
|
||||
FUNCTION_TEST_PARAM(STRING, ownerDefaultRoot);
|
||||
FUNCTION_TEST_END();
|
||||
|
||||
FUNCTION_TEST_RETURN_CONST(STRING, userRoot() ? (owner == NULL ? ownerDefaultRoot : owner) : NULL);
|
||||
}
|
||||
|
||||
// Helper to get list of owners from a file/link/path list
|
||||
#define RESTORE_MANIFEST_OWNER_GET(type, deref) \
|
||||
for (unsigned int itemIdx = 0; itemIdx < manifest##type##Total(manifest); itemIdx++) \
|
||||
{ \
|
||||
const Manifest##type item = deref manifest##type(manifest, itemIdx); \
|
||||
\
|
||||
if (item.user == NULL) \
|
||||
userNull = true; \
|
||||
else \
|
||||
strLstAddIfMissing(userList, item.user); \
|
||||
\
|
||||
if (item.group == NULL) \
|
||||
groupNull = true; \
|
||||
else \
|
||||
strLstAddIfMissing(groupList, item.group); \
|
||||
}
|
||||
|
||||
// Helper to warn when an owner is missing and must be remapped
|
||||
#define RESTORE_MANIFEST_OWNER_WARN(type) \
|
||||
do \
|
||||
{ \
|
||||
if (type##Null) \
|
||||
LOG_WARN("unknown " #type " in backup manifest mapped to current " #type); \
|
||||
\
|
||||
for (unsigned int ownerIdx = 0; ownerIdx < strLstSize(type##List); ownerIdx++) \
|
||||
{ \
|
||||
const String *const owner = strLstGet(type##List, ownerIdx); \
|
||||
\
|
||||
if (type##Name() == NULL || !strEq(type##Name(), owner)) \
|
||||
LOG_WARN_FMT("unknown " #type " '%s' in backup manifest mapped to current " #type, strZ(owner)); \
|
||||
} \
|
||||
} \
|
||||
while (0)
|
||||
|
||||
static void
|
||||
restoreManifestOwner(const Manifest *const manifest, const String **const rootReplaceUser, const String **const rootReplaceGroup)
|
||||
{
|
||||
FUNCTION_LOG_BEGIN(logLevelDebug);
|
||||
FUNCTION_LOG_PARAM(MANIFEST, manifest);
|
||||
FUNCTION_LOG_PARAM_P(VOID, rootReplaceUser);
|
||||
FUNCTION_LOG_PARAM_P(VOID, rootReplaceGroup);
|
||||
FUNCTION_LOG_END();
|
||||
|
||||
FUNCTION_AUDIT_HELPER();
|
||||
|
||||
ASSERT(manifest != NULL);
|
||||
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// Build a list of users and groups in the manifest
|
||||
// -------------------------------------------------------------------------------------------------------------------------
|
||||
bool userNull = false;
|
||||
StringList *const userList = strLstNew();
|
||||
bool groupNull = false;
|
||||
StringList *const groupList = strLstNew();
|
||||
|
||||
RESTORE_MANIFEST_OWNER_GET(File, );
|
||||
RESTORE_MANIFEST_OWNER_GET(Link, *(const ManifestLink *));
|
||||
RESTORE_MANIFEST_OWNER_GET(Path, *(const ManifestPath *));
|
||||
|
||||
// Update users and groups in the manifest (this can only be done as root)
|
||||
// -------------------------------------------------------------------------------------------------------------------------
|
||||
if (userRoot())
|
||||
{
|
||||
// Get user/group info from data directory to use for invalid user/groups
|
||||
StorageInfo pathInfo = storageInfoP(storagePg(), manifestTargetBase(manifest)->path, .ignoreMissing = true);
|
||||
|
||||
// If user/group is null then set it to root
|
||||
if (pathInfo.user == NULL) // {vm_covered}
|
||||
pathInfo.user = userName(); // {vm_covered}
|
||||
|
||||
if (pathInfo.group == NULL) // {vm_covered}
|
||||
pathInfo.group = groupName(); // {vm_covered}
|
||||
|
||||
if (userNull || groupNull)
|
||||
{
|
||||
if (userNull)
|
||||
LOG_WARN_FMT("unknown user in backup manifest mapped to '%s'", strZ(pathInfo.user));
|
||||
|
||||
if (groupNull)
|
||||
LOG_WARN_FMT("unknown group in backup manifest mapped to '%s'", strZ(pathInfo.group));
|
||||
|
||||
MEM_CONTEXT_PRIOR_BEGIN()
|
||||
{
|
||||
*rootReplaceUser = strDup(pathInfo.user);
|
||||
*rootReplaceGroup = strDup(pathInfo.group);
|
||||
}
|
||||
MEM_CONTEXT_PRIOR_END();
|
||||
}
|
||||
}
|
||||
// Else set owners to NULL. This means we won't make any attempt to update ownership and will just leave it as written by
|
||||
// the current user/group. If there are existing files that are not owned by the current user/group then we will attempt to
|
||||
// update them, which will generally cause an error, though some systems allow updates to the group ownership.
|
||||
// -------------------------------------------------------------------------------------------------------------------------
|
||||
else
|
||||
{
|
||||
RESTORE_MANIFEST_OWNER_WARN(user);
|
||||
RESTORE_MANIFEST_OWNER_WARN(group);
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
FUNCTION_LOG_RETURN_VOID();
|
||||
}
|
||||
|
||||
/***********************************************************************************************************************************
|
||||
Clean the data directory of any paths/files/links that are not in the manifest and create missing links/paths
|
||||
***********************************************************************************************************************************/
|
||||
typedef struct RestoreCleanCallbackData
|
||||
{
|
||||
const Manifest *manifest; // Manifest to compare against
|
||||
const ManifestTarget *target; // Current target being compared
|
||||
const String *targetName; // Name to use when finding files/paths/links
|
||||
const String *targetPath; // Path of target currently being compared
|
||||
const String *subPath; // Subpath in target currently being compared
|
||||
bool basePath; // Is this the base path?
|
||||
bool exists; // Does the target path exist?
|
||||
bool delta; // Is this a delta restore?
|
||||
StringList *fileIgnore; // Files to ignore during clean
|
||||
const String *rootReplaceUser; // User to replace invalid users when root
|
||||
const String *rootReplaceGroup; // Group to replace invalid group when root
|
||||
} RestoreCleanCallbackData;
|
||||
|
||||
// Helper to update ownership on a file/link/path
|
||||
static void
|
||||
restoreCleanOwnership(
|
||||
const String *const pgPath, const String *manifestUserName, const String *const rootReplaceUser,
|
||||
const String *manifestGroupName, const String *const rootReplaceGroup, const uid_t actualUserId, const gid_t actualGroupId,
|
||||
const bool new)
|
||||
{
|
||||
FUNCTION_TEST_BEGIN();
|
||||
FUNCTION_TEST_PARAM(STRING, pgPath);
|
||||
FUNCTION_TEST_PARAM(STRING, manifestUserName);
|
||||
FUNCTION_TEST_PARAM(STRING, manifestGroupName);
|
||||
FUNCTION_TEST_PARAM(UINT, actualUserId);
|
||||
FUNCTION_TEST_PARAM(UINT, actualGroupId);
|
||||
FUNCTION_TEST_PARAM(BOOL, new);
|
||||
FUNCTION_TEST_END();
|
||||
|
||||
ASSERT(pgPath != NULL);
|
||||
|
||||
// Get the expected user id
|
||||
uid_t expectedUserId = userId();
|
||||
|
||||
manifestUserName = restoreManifestOwnerReplace(manifestUserName, rootReplaceUser);
|
||||
|
||||
if (manifestUserName != NULL)
|
||||
{
|
||||
const uid_t manifestUserId = userIdFromName(manifestUserName);
|
||||
|
||||
if (manifestUserId != (uid_t)-1)
|
||||
expectedUserId = manifestUserId;
|
||||
}
|
||||
|
||||
// Get the expected group id
|
||||
gid_t expectedGroupId = groupId();
|
||||
|
||||
manifestGroupName = restoreManifestOwnerReplace(manifestGroupName, rootReplaceGroup);
|
||||
|
||||
if (manifestGroupName != NULL)
|
||||
{
|
||||
const uid_t manifestGroupId = groupIdFromName(manifestGroupName);
|
||||
|
||||
if (manifestGroupId != (uid_t)-1)
|
||||
expectedGroupId = manifestGroupId;
|
||||
}
|
||||
|
||||
// Update ownership if not as expected
|
||||
if (actualUserId != expectedUserId || actualGroupId != expectedGroupId)
|
||||
{
|
||||
// If this is a newly created file/link/path then there's no need to log updated permissions
|
||||
if (!new)
|
||||
LOG_DETAIL_FMT("update ownership for '%s'", strZ(pgPath));
|
||||
|
||||
THROW_ON_SYS_ERROR_FMT(
|
||||
lchown(strZ(pgPath), expectedUserId, expectedGroupId) == -1, FileOwnerError, "unable to set ownership for '%s'",
|
||||
strZ(pgPath));
|
||||
}
|
||||
|
||||
FUNCTION_TEST_RETURN_VOID();
|
||||
}
|
||||
|
||||
// Helper to update mode on a file/path
|
||||
static void
|
||||
restoreCleanMode(const String *const pgPath, const mode_t manifestMode, const StorageInfo *const info)
|
||||
{
|
||||
FUNCTION_TEST_BEGIN();
|
||||
FUNCTION_TEST_PARAM(STRING, pgPath);
|
||||
FUNCTION_TEST_PARAM(MODE, manifestMode);
|
||||
FUNCTION_TEST_PARAM(INFO, info);
|
||||
FUNCTION_TEST_END();
|
||||
|
||||
ASSERT(pgPath != NULL);
|
||||
ASSERT(info != NULL);
|
||||
|
||||
// Update mode if not as expected
|
||||
if (manifestMode != info->mode)
|
||||
{
|
||||
LOG_DETAIL_FMT("update mode for '%s' to %04o", strZ(pgPath), manifestMode);
|
||||
|
||||
THROW_ON_SYS_ERROR_FMT(
|
||||
chmod(strZ(pgPath), manifestMode) == -1, FileModeError, "unable to set mode for '%s'", strZ(pgPath));
|
||||
}
|
||||
|
||||
FUNCTION_TEST_RETURN_VOID();
|
||||
}
|
||||
|
||||
// Recurse paths
|
||||
static void
|
||||
restoreCleanBuildRecurse(StorageIterator *const storageItr, const RestoreCleanCallbackData *const cleanData)
|
||||
{
|
||||
FUNCTION_TEST_BEGIN();
|
||||
FUNCTION_TEST_PARAM(STORAGE_ITERATOR, storageItr);
|
||||
FUNCTION_TEST_PARAM_P(VOID, cleanData);
|
||||
FUNCTION_TEST_END();
|
||||
|
||||
ASSERT(storageItr != NULL);
|
||||
ASSERT(cleanData != NULL);
|
||||
|
||||
MEM_CONTEXT_TEMP_RESET_BEGIN()
|
||||
{
|
||||
while (storageItrMore(storageItr))
|
||||
{
|
||||
const StorageInfo info = storageItrNext(storageItr);
|
||||
|
||||
// Don't include backup.manifest or recovery.conf (when preserved) in the comparison or empty directory check
|
||||
if (cleanData->basePath && info.type == storageTypeFile && strLstExists(cleanData->fileIgnore, info.name))
|
||||
continue;
|
||||
|
||||
// If this is not a delta then error because the directory is expected to be empty. Ignore the . path.
|
||||
if (!cleanData->delta)
|
||||
{
|
||||
THROW_FMT(
|
||||
PathNotEmptyError,
|
||||
"unable to restore to path '%s' because it contains files\n"
|
||||
"HINT: try using --delta if this is what you intended.",
|
||||
strZ(cleanData->targetPath));
|
||||
}
|
||||
|
||||
// Construct the name used to find this file/link/path in the manifest
|
||||
const String *const manifestName = strNewFmt("%s/%s", strZ(cleanData->targetName), strZ(info.name));
|
||||
|
||||
// Construct the path of this file/link/path in the PostgreSQL data directory
|
||||
const String *const pgPath = strNewFmt("%s/%s", strZ(cleanData->targetPath), strZ(info.name));
|
||||
|
||||
switch (info.type)
|
||||
{
|
||||
case storageTypeFile:
|
||||
{
|
||||
if (manifestFileExists(cleanData->manifest, manifestName) &&
|
||||
manifestLinkFindDefault(cleanData->manifest, manifestName, NULL) == NULL)
|
||||
{
|
||||
const ManifestFile manifestFile = manifestFileFind(cleanData->manifest, manifestName);
|
||||
|
||||
restoreCleanOwnership(
|
||||
pgPath, manifestFile.user, cleanData->rootReplaceUser, manifestFile.group, cleanData->rootReplaceGroup,
|
||||
info.userId, info.groupId, false);
|
||||
restoreCleanMode(pgPath, manifestFile.mode, &info);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_DETAIL_FMT("remove invalid file '%s'", strZ(pgPath));
|
||||
storageRemoveP(storageLocalWrite(), pgPath, .errorOnMissing = true);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case storageTypeLink:
|
||||
{
|
||||
const ManifestLink *const manifestLink = manifestLinkFindDefault(cleanData->manifest, manifestName, NULL);
|
||||
|
||||
if (manifestLink != NULL)
|
||||
{
|
||||
if (!strEq(manifestLink->destination, info.linkDestination))
|
||||
{
|
||||
LOG_DETAIL_FMT("remove link '%s' because destination changed", strZ(pgPath));
|
||||
storageRemoveP(storageLocalWrite(), pgPath, .errorOnMissing = true);
|
||||
}
|
||||
else
|
||||
{
|
||||
restoreCleanOwnership(
|
||||
pgPath, manifestLink->user, cleanData->rootReplaceUser, manifestLink->group,
|
||||
cleanData->rootReplaceGroup, info.userId, info.groupId, false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_DETAIL_FMT("remove invalid link '%s'", strZ(pgPath));
|
||||
storageRemoveP(storageLocalWrite(), pgPath, .errorOnMissing = true);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case storageTypePath:
|
||||
{
|
||||
const ManifestPath *const manifestPath = manifestPathFindDefault(cleanData->manifest, manifestName, NULL);
|
||||
|
||||
if (manifestPath != NULL && manifestLinkFindDefault(cleanData->manifest, manifestName, NULL) == NULL)
|
||||
{
|
||||
// Check ownership/permissions
|
||||
restoreCleanOwnership(
|
||||
pgPath, manifestPath->user, cleanData->rootReplaceUser, manifestPath->group,
|
||||
cleanData->rootReplaceGroup, info.userId, info.groupId, false);
|
||||
restoreCleanMode(pgPath, manifestPath->mode, &info);
|
||||
|
||||
// Recurse into the path
|
||||
RestoreCleanCallbackData cleanDataSub = *cleanData;
|
||||
cleanDataSub.targetName = strNewFmt("%s/%s", strZ(cleanData->targetName), strZ(info.name));
|
||||
cleanDataSub.targetPath = strNewFmt("%s/%s", strZ(cleanData->targetPath), strZ(info.name));
|
||||
cleanDataSub.basePath = false;
|
||||
|
||||
restoreCleanBuildRecurse(
|
||||
storageNewItrP(
|
||||
storageLocalWrite(), cleanDataSub.targetPath, .errorOnMissing = true, .sortOrder = sortOrderAsc),
|
||||
&cleanDataSub);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_DETAIL_FMT("remove invalid path '%s'", strZ(pgPath));
|
||||
storagePathRemoveP(storageLocalWrite(), pgPath, .errorOnMissing = true, .recurse = true);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Special file types cannot exist in the manifest so just delete them
|
||||
case storageTypeSpecial:
|
||||
LOG_DETAIL_FMT("remove special file '%s'", strZ(pgPath));
|
||||
storageRemoveP(storageLocalWrite(), pgPath, .errorOnMissing = true);
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset the memory context occasionally so we don't use too much memory or slow down processing
|
||||
MEM_CONTEXT_TEMP_RESET(1000);
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
FUNCTION_TEST_RETURN_VOID();
|
||||
}
|
||||
|
||||
static void
|
||||
restoreCleanBuild(const Manifest *const manifest, const String *const rootReplaceUser, const String *const rootReplaceGroup)
|
||||
{
|
||||
FUNCTION_LOG_BEGIN(logLevelDebug);
|
||||
FUNCTION_LOG_PARAM(MANIFEST, manifest);
|
||||
FUNCTION_LOG_PARAM(STRING, rootReplaceUser);
|
||||
FUNCTION_LOG_PARAM(STRING, rootReplaceGroup);
|
||||
FUNCTION_LOG_END();
|
||||
|
||||
ASSERT(manifest != NULL);
|
||||
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// Is this a delta restore?
|
||||
const bool delta = cfgOptionBool(cfgOptDelta) || cfgOptionBool(cfgOptForce);
|
||||
|
||||
// Allocate data for each target
|
||||
RestoreCleanCallbackData *const cleanDataList = memNew(sizeof(RestoreCleanCallbackData) * manifestTargetTotal(manifest));
|
||||
|
||||
// Step 1: Check permissions and validity (is the directory empty without delta?) if the target directory exists
|
||||
// -------------------------------------------------------------------------------------------------------------------------
|
||||
StringList *const pathChecked = strLstNew();
|
||||
|
||||
for (unsigned int targetIdx = 0; targetIdx < manifestTargetTotal(manifest); targetIdx++)
|
||||
{
|
||||
RestoreCleanCallbackData *const cleanData = &cleanDataList[targetIdx];
|
||||
|
||||
*cleanData = (RestoreCleanCallbackData)
|
||||
{
|
||||
.manifest = manifest,
|
||||
.target = manifestTarget(manifest, targetIdx),
|
||||
.delta = delta,
|
||||
.fileIgnore = strLstNew(),
|
||||
.rootReplaceUser = rootReplaceUser,
|
||||
.rootReplaceGroup = rootReplaceGroup,
|
||||
};
|
||||
|
||||
cleanData->targetName = cleanData->target->name;
|
||||
cleanData->targetPath = manifestTargetPath(manifest, cleanData->target);
|
||||
cleanData->basePath = strEq(cleanData->targetName, MANIFEST_TARGET_PGDATA_STR);
|
||||
|
||||
// Ignore backup.manifest while cleaning since it may exist from an prior incomplete restore
|
||||
strLstAdd(cleanData->fileIgnore, BACKUP_MANIFEST_FILE_STR);
|
||||
|
||||
// Also ignore recovery files when recovery type = preserve
|
||||
if (cfgOptionSeq(cfgOptType) == CFGOPTVAL_RESTORE_TYPE_PRESERVE)
|
||||
{
|
||||
// If recovery GUCs then three files must be preserved
|
||||
if (manifestData(manifest)->pgVersion >= PG_VERSION_RECOVERY_GUC)
|
||||
{
|
||||
strLstAdd(cleanData->fileIgnore, PG_FILE_POSTGRESQLAUTOCONF_STR);
|
||||
strLstAdd(cleanData->fileIgnore, PG_FILE_RECOVERYSIGNAL_STR);
|
||||
strLstAdd(cleanData->fileIgnore, PG_FILE_STANDBYSIGNAL_STR);
|
||||
}
|
||||
// Else just recovery.conf
|
||||
else
|
||||
strLstAdd(cleanData->fileIgnore, PG_FILE_RECOVERYCONF_STR);
|
||||
}
|
||||
|
||||
// If this is a tablespace append the tablespace identifier
|
||||
if (cleanData->target->type == manifestTargetTypeLink && cleanData->target->tablespaceId != 0)
|
||||
{
|
||||
const String *const tablespaceId = pgTablespaceId(
|
||||
manifestData(manifest)->pgVersion, manifestData(manifest)->pgCatalogVersion);
|
||||
|
||||
cleanData->targetName = strNewFmt("%s/%s", strZ(cleanData->targetName), strZ(tablespaceId));
|
||||
cleanData->targetPath = strNewFmt("%s/%s", strZ(cleanData->targetPath), strZ(tablespaceId));
|
||||
}
|
||||
|
||||
strLstSort(cleanData->fileIgnore, sortOrderAsc);
|
||||
|
||||
// Check that the path exists. If not, there's no need to do any cleaning and we'll attempt to create it later. Don't
|
||||
// log check for the same path twice. There can be multiple links to files in the same path, but logging it more than
|
||||
// once makes the logs noisy and looks like a bug.
|
||||
if (!strLstExists(pathChecked, cleanData->targetPath))
|
||||
LOG_DETAIL_FMT("check '%s' exists", strZ(cleanData->targetPath));
|
||||
|
||||
const StorageInfo info = storageInfoP(storageLocal(), cleanData->targetPath, .ignoreMissing = true, .followLink = true);
|
||||
strLstAdd(pathChecked, cleanData->targetPath);
|
||||
|
||||
if (info.exists)
|
||||
{
|
||||
// Make sure our uid will be able to write to this directory
|
||||
if (!userRoot() && userId() != info.userId)
|
||||
{
|
||||
THROW_FMT(
|
||||
PathOpenError, "unable to restore to path '%s' not owned by current user", strZ(cleanData->targetPath));
|
||||
}
|
||||
|
||||
if ((info.mode & 0700) != 0700)
|
||||
{
|
||||
THROW_FMT(
|
||||
PathOpenError, "unable to restore to path '%s' without rwx permissions", strZ(cleanData->targetPath));
|
||||
}
|
||||
|
||||
// If not a delta restore then check that the directories are empty, or if a file link, that the file doesn't exist
|
||||
if (!cleanData->delta)
|
||||
{
|
||||
if (cleanData->target->file == NULL)
|
||||
{
|
||||
restoreCleanBuildRecurse(
|
||||
storageNewItrP(storageLocal(), cleanData->targetPath, .errorOnMissing = true), cleanData);
|
||||
}
|
||||
else
|
||||
{
|
||||
const String *const file = strNewFmt("%s/%s", strZ(cleanData->targetPath), strZ(cleanData->target->file));
|
||||
|
||||
if (storageExistsP(storageLocal(), file))
|
||||
{
|
||||
THROW_FMT(
|
||||
FileExistsError,
|
||||
"unable to restore file '%s' because it already exists\n"
|
||||
"HINT: try using --delta if this is what you intended.",
|
||||
strZ(file));
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we know there are no files in this target enable delta for processing in step 2
|
||||
cleanData->delta = true;
|
||||
}
|
||||
|
||||
// The target directory exists and is valid and will need to be cleaned
|
||||
cleanData->exists = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip the tablespace_map file when present so PostgreSQL does not rewrite links in pg_tblspc. The tablespace links will be
|
||||
// created after paths are cleaned.
|
||||
if (manifestFileExists(manifest, STRDEF(MANIFEST_TARGET_PGDATA "/" PG_FILE_TABLESPACEMAP)))
|
||||
{
|
||||
LOG_DETAIL_FMT("skip '" PG_FILE_TABLESPACEMAP "' -- tablespace links will be created based on mappings");
|
||||
manifestFileRemove(manifest, STRDEF(MANIFEST_TARGET_PGDATA "/" PG_FILE_TABLESPACEMAP));
|
||||
}
|
||||
|
||||
// Skip postgresql.auto.conf if preserve is set and the PostgreSQL version supports recovery GUCs
|
||||
if (manifestData(manifest)->pgVersion >= PG_VERSION_RECOVERY_GUC &&
|
||||
cfgOptionSeq(cfgOptType) == CFGOPTVAL_RESTORE_TYPE_PRESERVE &&
|
||||
manifestFileExists(manifest, STRDEF(MANIFEST_TARGET_PGDATA "/" PG_FILE_POSTGRESQLAUTOCONF)))
|
||||
{
|
||||
LOG_DETAIL_FMT("skip '" PG_FILE_POSTGRESQLAUTOCONF "' -- recovery type is preserve");
|
||||
manifestFileRemove(manifest, STRDEF(MANIFEST_TARGET_PGDATA "/" PG_FILE_POSTGRESQLAUTOCONF));
|
||||
}
|
||||
|
||||
// Step 2: Clean target directories
|
||||
// -------------------------------------------------------------------------------------------------------------------------
|
||||
// Delete the pg_control file (if it exists) so the cluster cannot be started if restore does not complete. Sync the path so
|
||||
// the file does not return, zombie-like, in the case of a host crash.
|
||||
if (storageExistsP(storagePg(), STRDEF(PG_PATH_GLOBAL "/" PG_FILE_PGCONTROL)))
|
||||
{
|
||||
LOG_DETAIL_FMT(
|
||||
"remove '" PG_PATH_GLOBAL "/" PG_FILE_PGCONTROL "' so cluster will not start if restore does not complete");
|
||||
storageRemoveP(storagePgWrite(), STRDEF(PG_PATH_GLOBAL "/" PG_FILE_PGCONTROL));
|
||||
storagePathSyncP(storagePgWrite(), PG_PATH_GLOBAL_STR);
|
||||
}
|
||||
|
||||
for (unsigned int targetIdx = 0; targetIdx < manifestTargetTotal(manifest); targetIdx++)
|
||||
{
|
||||
const RestoreCleanCallbackData *const cleanData = &cleanDataList[targetIdx];
|
||||
|
||||
// Only clean if the target exists
|
||||
if (cleanData->exists)
|
||||
{
|
||||
// Don't clean file links. It doesn't matter whether the file exists or not since we know it is in the manifest.
|
||||
if (cleanData->target->file == NULL)
|
||||
{
|
||||
// Only log when doing a delta restore because otherwise the targets should be empty. We'll still run the clean
|
||||
// to fix permissions/ownership on the target paths.
|
||||
if (delta)
|
||||
LOG_INFO_FMT("remove invalid files/links/paths from '%s'", strZ(cleanData->targetPath));
|
||||
|
||||
// Check target ownership/permissions
|
||||
const ManifestPath *const manifestPath = manifestPathFind(cleanData->manifest, cleanData->targetName);
|
||||
const StorageInfo info = storageInfoP(storageLocal(), cleanData->targetPath, .followLink = true);
|
||||
|
||||
restoreCleanOwnership(
|
||||
cleanData->targetPath, manifestPath->user, rootReplaceUser, manifestPath->group, rootReplaceGroup,
|
||||
info.userId, info.groupId, false);
|
||||
restoreCleanMode(cleanData->targetPath, manifestPath->mode, &info);
|
||||
|
||||
// Clean the target
|
||||
restoreCleanBuildRecurse(
|
||||
storageNewItrP(
|
||||
storageLocalWrite(), cleanData->targetPath, .errorOnMissing = true, .sortOrder = sortOrderAsc),
|
||||
cleanData);
|
||||
}
|
||||
}
|
||||
// If the target does not exist we'll attempt to create it
|
||||
else
|
||||
{
|
||||
const ManifestPath *path = NULL;
|
||||
|
||||
// There is no path information for a file link so we'll need to use the data directory
|
||||
if (cleanData->target->file != NULL)
|
||||
{
|
||||
path = manifestPathFind(manifest, MANIFEST_TARGET_PGDATA_STR);
|
||||
}
|
||||
// Else grab the info for the path that matches the link name
|
||||
else
|
||||
path = manifestPathFind(manifest, cleanData->target->name);
|
||||
|
||||
storagePathCreateP(storageLocalWrite(), cleanData->targetPath, .mode = path->mode);
|
||||
restoreCleanOwnership(
|
||||
cleanData->targetPath, path->user, rootReplaceUser, path->group, rootReplaceGroup, userId(), groupId(), true);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Create missing paths and path links
|
||||
// -------------------------------------------------------------------------------------------------------------------------
|
||||
for (unsigned int pathIdx = 0; pathIdx < manifestPathTotal(manifest); pathIdx++)
|
||||
{
|
||||
const ManifestPath *const path = manifestPath(manifest, pathIdx);
|
||||
|
||||
// Skip the pg_tblspc path because it only maps to the manifest. We should remove this in a future release but not much
|
||||
// can be done about it for now.
|
||||
if (strEq(path->name, MANIFEST_TARGET_PGTBLSPC_STR))
|
||||
continue;
|
||||
|
||||
// If this path has been mapped as a link then create a link. The path has already been created as part of target
|
||||
// creation (or it might have already existed).
|
||||
const ManifestLink *const link = manifestLinkFindDefault(
|
||||
manifest,
|
||||
strBeginsWith(path->name, MANIFEST_TARGET_PGTBLSPC_STR) ?
|
||||
strNewFmt(MANIFEST_TARGET_PGDATA "/%s", strZ(path->name)) : path->name,
|
||||
NULL);
|
||||
|
||||
if (link != NULL)
|
||||
{
|
||||
const String *const pgPath = storagePathP(storagePg(), manifestPathPg(link->name));
|
||||
const StorageInfo linkInfo = storageInfoP(storagePg(), pgPath, .ignoreMissing = true);
|
||||
|
||||
// Create the link if it is missing. If it exists it should already have the correct ownership and destination.
|
||||
if (!linkInfo.exists)
|
||||
{
|
||||
LOG_DETAIL_FMT("create symlink '%s' to '%s'", strZ(pgPath), strZ(link->destination));
|
||||
|
||||
storageLinkCreateP(storagePgWrite(), link->destination, pgPath);
|
||||
restoreCleanOwnership(
|
||||
pgPath, link->user, rootReplaceUser, link->group, rootReplaceGroup, userId(), groupId(), true);
|
||||
}
|
||||
}
|
||||
// Create the path normally
|
||||
else
|
||||
{
|
||||
const String *const pgPath = storagePathP(storagePg(), manifestPathPg(path->name));
|
||||
const StorageInfo pathInfo = storageInfoP(storagePg(), pgPath, .ignoreMissing = true);
|
||||
|
||||
// Create the path if it is missing. If it exists it should already have the correct ownership and mode.
|
||||
if (!pathInfo.exists)
|
||||
{
|
||||
LOG_DETAIL_FMT("create path '%s'", strZ(pgPath));
|
||||
|
||||
storagePathCreateP(storagePgWrite(), pgPath, .mode = path->mode, .noParentCreate = true, .errorOnExists = true);
|
||||
restoreCleanOwnership(
|
||||
storagePathP(storagePg(), pgPath), path->user, rootReplaceUser, path->group, rootReplaceGroup, userId(),
|
||||
groupId(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Create file links. These don't get created during path creation because they do not have a matching path entry.
|
||||
// -------------------------------------------------------------------------------------------------------------------------
|
||||
for (unsigned int linkIdx = 0; linkIdx < manifestLinkTotal(manifest); linkIdx++)
|
||||
{
|
||||
const ManifestLink *const link = manifestLink(manifest, linkIdx);
|
||||
const String *const pgPath = storagePathP(storagePg(), manifestPathPg(link->name));
|
||||
const StorageInfo linkInfo = storageInfoP(storagePg(), pgPath, .ignoreMissing = true);
|
||||
|
||||
// Create the link if it is missing. If it exists it should already have the correct ownership and destination.
|
||||
if (!linkInfo.exists)
|
||||
{
|
||||
LOG_DETAIL_FMT("create symlink '%s' to '%s'", strZ(pgPath), strZ(link->destination));
|
||||
|
||||
storageLinkCreateP(storagePgWrite(), link->destination, pgPath);
|
||||
restoreCleanOwnership(
|
||||
pgPath, link->user, rootReplaceUser, link->group, rootReplaceGroup, userId(), groupId(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
FUNCTION_LOG_RETURN_VOID();
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
/***********************************************************************************************************************************
|
||||
Recovery constants
|
||||
***********************************************************************************************************************************/
|
||||
#define RESTORE_COMMAND "restore_command"
|
||||
STRING_STATIC(RESTORE_COMMAND_STR, RESTORE_COMMAND);
|
||||
|
||||
#define RECOVERY_TARGET "recovery_target"
|
||||
#define RECOVERY_TARGET_LSN "recovery_target_lsn"
|
||||
#define RECOVERY_TARGET_NAME "recovery_target_name"
|
||||
#define RECOVERY_TARGET_TIME "recovery_target_time"
|
||||
#define RECOVERY_TARGET_XID "recovery_target_xid"
|
||||
|
||||
#define RECOVERY_TARGET_ACTION "recovery_target_action"
|
||||
#define RECOVERY_TARGET_INCLUSIVE "recovery_target_inclusive"
|
||||
|
||||
#define RECOVERY_TARGET_TIMELINE "recovery_target_timeline"
|
||||
#define RECOVERY_TARGET_TIMELINE_CURRENT "current"
|
||||
|
||||
#define STANDBY_MODE "standby_mode"
|
||||
STRING_STATIC(STANDBY_MODE_STR, STANDBY_MODE);
|
||||
|
||||
#define ARCHIVE_MODE "archive_mode"
|
||||
|
||||
/***********************************************************************************************************************************
|
||||
Generate the recovery file
|
||||
***********************************************************************************************************************************/
|
||||
// Helper to generate recovery options
|
||||
static KeyValue *
|
||||
restoreRecoveryOption(const unsigned int pgVersion)
|
||||
{
|
||||
FUNCTION_LOG_BEGIN(logLevelDebug);
|
||||
FUNCTION_LOG_PARAM(UINT, pgVersion);
|
||||
FUNCTION_LOG_END();
|
||||
|
||||
KeyValue *result = NULL;
|
||||
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
result = kvNew();
|
||||
|
||||
StringList *recoveryOptionKey = strLstNew();
|
||||
|
||||
if (cfgOptionTest(cfgOptRecoveryOption))
|
||||
{
|
||||
const KeyValue *const recoveryOption = cfgOptionKv(cfgOptRecoveryOption);
|
||||
recoveryOptionKey = strLstSort(strLstNewVarLst(kvKeyList(recoveryOption)), sortOrderAsc);
|
||||
|
||||
for (unsigned int keyIdx = 0; keyIdx < strLstSize(recoveryOptionKey); keyIdx++)
|
||||
{
|
||||
// Get the key and value
|
||||
String *const key = strLstGet(recoveryOptionKey, keyIdx);
|
||||
const String *const value = varStr(kvGet(recoveryOption, VARSTR(key)));
|
||||
|
||||
// Replace - in key with _. Since we use - users naturally will as well.
|
||||
strReplaceChr(key, '-', '_');
|
||||
|
||||
kvPut(result, VARSTR(key), VARSTR(value));
|
||||
}
|
||||
|
||||
strLstSort(recoveryOptionKey, sortOrderAsc);
|
||||
}
|
||||
|
||||
// If archive-mode is not preserve
|
||||
if (cfgOptionSeq(cfgOptArchiveMode) != CFGOPTVAL_ARCHIVE_MODE_PRESERVE)
|
||||
{
|
||||
if (pgVersion < PG_VERSION_12)
|
||||
{
|
||||
THROW_FMT(
|
||||
OptionInvalidError,
|
||||
"option '" CFGOPT_ARCHIVE_MODE "' is not supported on " PG_NAME " < " PG_VERSION_12_Z "\n"
|
||||
"HINT: 'archive_mode' should be manually set to 'off' in postgresql.conf.");
|
||||
}
|
||||
|
||||
// The only other valid option is off
|
||||
ASSERT(cfgOptionSeq(cfgOptArchiveMode) == CFGOPTVAL_ARCHIVE_MODE_OFF);
|
||||
|
||||
// If archive-mode=off then set archive_mode=off
|
||||
kvPut(result, VARSTRDEF(ARCHIVE_MODE), VARSTRDEF(CFGOPTVAL_ARCHIVE_MODE_OFF_Z));
|
||||
}
|
||||
|
||||
// Write restore_command
|
||||
if (!strLstExists(recoveryOptionKey, RESTORE_COMMAND_STR))
|
||||
{
|
||||
// Null out options that it does not make sense to pass from the restore command to archive-get. All of these have
|
||||
// reasonable defaults so there is no danger of an error -- they just might not be optimal. In any case, it seems better
|
||||
// than, for example, passing --process-max=32 to archive-get because it was specified for restore.
|
||||
KeyValue *const optionReplace = kvNew();
|
||||
|
||||
kvPut(optionReplace, VARSTRDEF(CFGOPT_EXEC_ID), NULL);
|
||||
kvPut(optionReplace, VARSTRDEF(CFGOPT_JOB_RETRY), NULL);
|
||||
kvPut(optionReplace, VARSTRDEF(CFGOPT_JOB_RETRY_INTERVAL), NULL);
|
||||
kvPut(optionReplace, VARSTRDEF(CFGOPT_LOG_LEVEL_CONSOLE), NULL);
|
||||
kvPut(optionReplace, VARSTRDEF(CFGOPT_LOG_LEVEL_FILE), NULL);
|
||||
kvPut(optionReplace, VARSTRDEF(CFGOPT_LOG_LEVEL_STDERR), NULL);
|
||||
kvPut(optionReplace, VARSTRDEF(CFGOPT_LOG_SUBPROCESS), NULL);
|
||||
kvPut(optionReplace, VARSTRDEF(CFGOPT_LOG_TIMESTAMP), NULL);
|
||||
kvPut(optionReplace, VARSTRDEF(CFGOPT_PROCESS_MAX), NULL);
|
||||
kvPut(optionReplace, VARSTRDEF(CFGOPT_CMD), NULL);
|
||||
|
||||
kvPut(
|
||||
result, VARSTRDEF(RESTORE_COMMAND),
|
||||
VARSTR(
|
||||
strNewFmt(
|
||||
"%s %s %%f \"%%p\"", strZ(cfgOptionStr(cfgOptCmd)),
|
||||
strZ(strLstJoin(cfgExecParam(cfgCmdArchiveGet, cfgCmdRoleMain, optionReplace, true, true), " ")))));
|
||||
}
|
||||
|
||||
// If recovery type is immediate
|
||||
if (cfgOptionSeq(cfgOptType) == CFGOPTVAL_RESTORE_TYPE_IMMEDIATE)
|
||||
{
|
||||
kvPut(result, VARSTRDEF(RECOVERY_TARGET), VARSTRDEF(CFGOPTVAL_RESTORE_TYPE_IMMEDIATE_Z));
|
||||
}
|
||||
// Else recovery type is standby
|
||||
else if (cfgOptionSeq(cfgOptType) == CFGOPTVAL_RESTORE_TYPE_STANDBY)
|
||||
{
|
||||
// Write standby_mode for PostgreSQL versions that support it
|
||||
if (pgVersion < PG_VERSION_RECOVERY_GUC)
|
||||
kvPut(result, VARSTR(STANDBY_MODE_STR), VARSTRDEF("on"));
|
||||
}
|
||||
// Else recovery type is not default so write target options
|
||||
else if (cfgOptionSeq(cfgOptType) != CFGOPTVAL_RESTORE_TYPE_DEFAULT)
|
||||
{
|
||||
// Write the recovery target
|
||||
kvPut(
|
||||
result, VARSTR(strNewFmt(RECOVERY_TARGET "_%s", strZ(cfgOptionDisplay(cfgOptType)))),
|
||||
VARSTR(cfgOptionStr(cfgOptTarget)));
|
||||
|
||||
// Write recovery_target_inclusive
|
||||
if (cfgOptionTest(cfgOptTargetExclusive) && cfgOptionBool(cfgOptTargetExclusive))
|
||||
kvPut(result, VARSTRDEF(RECOVERY_TARGET_INCLUSIVE), VARSTR(FALSE_STR));
|
||||
}
|
||||
|
||||
// Write recovery_target_action
|
||||
if (cfgOptionTest(cfgOptTargetAction))
|
||||
{
|
||||
const StringId targetAction = cfgOptionStrId(cfgOptTargetAction);
|
||||
|
||||
if (targetAction != CFGOPTVAL_TARGET_ACTION_PAUSE)
|
||||
{
|
||||
kvPut(result, VARSTRDEF(RECOVERY_TARGET_ACTION), VARSTR(strNewStrId(targetAction)));
|
||||
}
|
||||
}
|
||||
|
||||
// Write recovery_target_timeline if set
|
||||
if (cfgOptionTest(cfgOptTargetTimeline))
|
||||
{
|
||||
// Do not set current when PostgreSQL < 12 since this is the default and if current is explicitly set it acts as latest
|
||||
if (pgVersion >= PG_VERSION_12 || !strEqZ(cfgOptionStr(cfgOptTargetTimeline), RECOVERY_TARGET_TIMELINE_CURRENT))
|
||||
kvPut(result, VARSTRDEF(RECOVERY_TARGET_TIMELINE), VARSTR(cfgOptionStr(cfgOptTargetTimeline)));
|
||||
}
|
||||
// Else explicitly set target timeline to "current" when type=immediate and PostgreSQL >= 12. We do this because
|
||||
// type=immediate means there won't be any actual attempt to change timelines, but if we leave the target timeline as the
|
||||
// default of "latest" then PostgreSQL might fail to restore because it can't reach the "latest" timeline in the repository
|
||||
// from this backup.
|
||||
//
|
||||
// This is really a PostgreSQL bug and will hopefully be addressed there, but we'll handle it here for older versions, at
|
||||
// least until they aren't really seen in the wild any longer.
|
||||
//
|
||||
// PostgreSQL < 12 defaults to "current" (but does not accept "current" as a parameter) so no need set it explicitly.
|
||||
else if (cfgOptionSeq(cfgOptType) == CFGOPTVAL_RESTORE_TYPE_IMMEDIATE && pgVersion >= PG_VERSION_12)
|
||||
kvPut(result, VARSTRDEF(RECOVERY_TARGET_TIMELINE), VARSTRDEF(RECOVERY_TARGET_TIMELINE_CURRENT));
|
||||
|
||||
// Move to prior context
|
||||
kvMove(result, memContextPrior());
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
FUNCTION_LOG_RETURN(KEY_VALUE, result);
|
||||
}
|
||||
|
||||
// Helper to convert recovery options to text format
|
||||
static String *
|
||||
restoreRecoveryConf(const unsigned int pgVersion, const String *const restoreLabel)
|
||||
{
|
||||
FUNCTION_LOG_BEGIN(logLevelDebug);
|
||||
FUNCTION_LOG_PARAM(UINT, pgVersion);
|
||||
FUNCTION_LOG_PARAM(STRING, restoreLabel);
|
||||
FUNCTION_LOG_END();
|
||||
|
||||
String *const result = strNew();
|
||||
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
strCatFmt(result, "# Recovery settings generated by " PROJECT_NAME " restore on %s\n", strZ(restoreLabel));
|
||||
|
||||
// Output all recovery options
|
||||
const KeyValue *const optionKv = restoreRecoveryOption(pgVersion);
|
||||
const VariantList *const optionKeyList = kvKeyList(optionKv);
|
||||
|
||||
for (unsigned int optionKeyIdx = 0; optionKeyIdx < varLstSize(optionKeyList); optionKeyIdx++)
|
||||
{
|
||||
const Variant *const optionKey = varLstGet(optionKeyList, optionKeyIdx);
|
||||
|
||||
strCatFmt(result, "%s = '%s'\n", strZ(varStr(optionKey)), strZ(varStr(kvGet(optionKv, optionKey))));
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
FUNCTION_LOG_RETURN(STRING, result);
|
||||
}
|
||||
|
||||
// Helper to write recovery options into recovery.conf
|
||||
static void
|
||||
restoreRecoveryWriteConf(
|
||||
const Manifest *const manifest, const StorageInfo *const fileInfo, const unsigned int pgVersion,
|
||||
const String *const restoreLabel)
|
||||
{
|
||||
FUNCTION_LOG_BEGIN(logLevelDebug);
|
||||
FUNCTION_LOG_PARAM(MANIFEST, manifest);
|
||||
FUNCTION_LOG_PARAM(STORAGE_INFO, fileInfo);
|
||||
FUNCTION_LOG_PARAM(UINT, pgVersion);
|
||||
FUNCTION_LOG_PARAM(STRING, restoreLabel);
|
||||
FUNCTION_LOG_END();
|
||||
|
||||
// Only write recovery.conf if recovery type != none
|
||||
if (cfgOptionSeq(cfgOptType) != CFGOPTVAL_RESTORE_TYPE_NONE)
|
||||
{
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
LOG_INFO_FMT("write %s", strZ(storagePathP(storagePg(), PG_FILE_RECOVERYCONF_STR)));
|
||||
|
||||
// Write recovery.conf
|
||||
storagePutP(
|
||||
storageNewWriteP(
|
||||
storagePgWrite(), PG_FILE_RECOVERYCONF_STR, .noCreatePath = true, .modeFile = fileInfo->mode, .noAtomic = true,
|
||||
.noSyncPath = true, .user = fileInfo->user, .group = fileInfo->group),
|
||||
BUFSTR(restoreRecoveryConf(pgVersion, restoreLabel)));
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
}
|
||||
|
||||
FUNCTION_LOG_RETURN_VOID();
|
||||
}
|
||||
|
||||
// Helper to write recovery options into postgresql.auto.conf
|
||||
static void
|
||||
restoreRecoveryWriteAutoConf(
|
||||
const Manifest *const manifest, const StorageInfo *const fileInfo, const unsigned int pgVersion,
|
||||
const String *const restoreLabel)
|
||||
{
|
||||
FUNCTION_LOG_BEGIN(logLevelDebug);
|
||||
FUNCTION_LOG_PARAM(MANIFEST, manifest);
|
||||
FUNCTION_LOG_PARAM(STORAGE_INFO, fileInfo);
|
||||
FUNCTION_LOG_PARAM(UINT, pgVersion);
|
||||
FUNCTION_LOG_PARAM(STRING, restoreLabel);
|
||||
FUNCTION_LOG_END();
|
||||
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
String *const content = strNew();
|
||||
|
||||
// Load postgresql.auto.conf so we can preserve the existing contents
|
||||
const Buffer *const autoConf = storageGetP(
|
||||
storageNewReadP(storagePg(), PG_FILE_POSTGRESQLAUTOCONF_STR, .ignoreMissing = true));
|
||||
|
||||
// It is unusual for the file not to exist, but we'll continue processing by creating a blank file
|
||||
if (autoConf == NULL)
|
||||
{
|
||||
LOG_WARN(PG_FILE_POSTGRESQLAUTOCONF " does not exist -- creating to contain recovery settings");
|
||||
}
|
||||
// Else the file does exist so comment out old recovery options that could interfere with the current recovery. Don't
|
||||
// comment out *all* recovery options because some should only be commented out if there is a new option to replace it, e.g.
|
||||
// primary_conninfo. If the option shouldn't be commented out all the time then it won't ever be commented out -- this may
|
||||
// not be ideal but it is what was decided. PostgreSQL will use the last value set so this is safe as long as the option
|
||||
// does not have dependencies on other options.
|
||||
else
|
||||
{
|
||||
// Generate a regexp that will match on all current recovery_target settings
|
||||
RegExp *const recoveryExp =
|
||||
regExpNew(
|
||||
STRDEF(
|
||||
"^[\t ]*(" RECOVERY_TARGET "|" RECOVERY_TARGET_ACTION "|" RECOVERY_TARGET_INCLUSIVE "|"
|
||||
RECOVERY_TARGET_LSN "|" RECOVERY_TARGET_NAME "|" RECOVERY_TARGET_TIME "|" RECOVERY_TARGET_TIMELINE "|"
|
||||
RECOVERY_TARGET_XID ")[\t ]*="));
|
||||
|
||||
// Check each line for recovery settings
|
||||
const StringList *const contentList = strLstNewSplit(strNewBuf(autoConf), LF_STR);
|
||||
|
||||
for (unsigned int contentIdx = 0; contentIdx < strLstSize(contentList); contentIdx++)
|
||||
{
|
||||
if (contentIdx != 0)
|
||||
strCat(content, LF_STR);
|
||||
|
||||
const String *const line = strLstGet(contentList, contentIdx);
|
||||
|
||||
if (regExpMatch(recoveryExp, line))
|
||||
strCatFmt(content, "# Removed by " PROJECT_NAME " restore on %s # ", strZ(restoreLabel));
|
||||
|
||||
strCat(content, line);
|
||||
}
|
||||
|
||||
// If settings will be appended then format the file so a blank line will be between old and new settings
|
||||
if (cfgOptionSeq(cfgOptType) != CFGOPTVAL_RESTORE_TYPE_NONE)
|
||||
{
|
||||
strTrim(content);
|
||||
strCatZ(content, "\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
// If recovery was requested then write the recovery options
|
||||
if (cfgOptionSeq(cfgOptType) != CFGOPTVAL_RESTORE_TYPE_NONE)
|
||||
{
|
||||
// If the user specified standby_mode as a recovery option then error. It's tempting to just set type=standby in this
|
||||
// case but since config parsing has already happened the target options could be in an invalid state.
|
||||
if (cfgOptionTest(cfgOptRecoveryOption))
|
||||
{
|
||||
const KeyValue *const recoveryOption = cfgOptionKv(cfgOptRecoveryOption);
|
||||
const StringList *const recoveryOptionKey = strLstNewVarLst(kvKeyList(recoveryOption));
|
||||
|
||||
for (unsigned int keyIdx = 0; keyIdx < strLstSize(recoveryOptionKey); keyIdx++)
|
||||
{
|
||||
// Get the key and value
|
||||
String *const key = strLstGet(recoveryOptionKey, keyIdx);
|
||||
|
||||
// Replace - in key with _. Since we use - users naturally will as well.
|
||||
strReplaceChr(key, '-', '_');
|
||||
|
||||
if (strEq(key, STANDBY_MODE_STR))
|
||||
{
|
||||
THROW_FMT(
|
||||
OptionInvalidError,
|
||||
"'" STANDBY_MODE "' setting is not valid for " PG_NAME " >= %s\n"
|
||||
"HINT: use --" CFGOPT_TYPE "=" CFGOPTVAL_RESTORE_TYPE_STANDBY_Z " instead of --" CFGOPT_RECOVERY_OPTION
|
||||
"=" STANDBY_MODE "=on.",
|
||||
strZ(pgVersionToStr(PG_VERSION_RECOVERY_GUC)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
strCat(content, restoreRecoveryConf(pgVersion, restoreLabel));
|
||||
}
|
||||
|
||||
LOG_INFO_FMT(
|
||||
"write %s%s", autoConf == NULL ? "" : "updated ", strZ(storagePathP(storagePg(), PG_FILE_POSTGRESQLAUTOCONF_STR)));
|
||||
|
||||
// Write postgresql.auto.conf
|
||||
storagePutP(
|
||||
storageNewWriteP(
|
||||
storagePgWrite(), PG_FILE_POSTGRESQLAUTOCONF_STR, .noCreatePath = true, .modeFile = fileInfo->mode,
|
||||
.noAtomic = true, .noSyncPath = true, .user = fileInfo->user, .group = fileInfo->group),
|
||||
BUFSTR(content));
|
||||
|
||||
// The standby.signal file is required for standby mode
|
||||
if (cfgOptionSeq(cfgOptType) == CFGOPTVAL_RESTORE_TYPE_STANDBY)
|
||||
{
|
||||
storagePutP(
|
||||
storageNewWriteP(
|
||||
storagePgWrite(), PG_FILE_STANDBYSIGNAL_STR, .noCreatePath = true, .modeFile = fileInfo->mode,
|
||||
.noAtomic = true, .noSyncPath = true, .user = fileInfo->user, .group = fileInfo->group),
|
||||
NULL);
|
||||
}
|
||||
// Else the recovery.signal file is required for targeted recovery. Skip writing this file if the backup was offline and
|
||||
// recovery type is none since PostgreSQL will error in this case when wal_level=minimal.
|
||||
else if (cfgOptionSeq(cfgOptType) != CFGOPTVAL_RESTORE_TYPE_NONE)
|
||||
{
|
||||
storagePutP(
|
||||
storageNewWriteP(
|
||||
storagePgWrite(), PG_FILE_RECOVERYSIGNAL_STR, .noCreatePath = true, .modeFile = fileInfo->mode,
|
||||
.noAtomic = true, .noSyncPath = true, .user = fileInfo->user, .group = fileInfo->group),
|
||||
NULL);
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
FUNCTION_LOG_RETURN_VOID();
|
||||
}
|
||||
|
||||
static void
|
||||
restoreRecoveryWrite(const Manifest *const manifest, const StorageInfo *const fileInfo)
|
||||
{
|
||||
FUNCTION_LOG_BEGIN(logLevelDebug);
|
||||
FUNCTION_LOG_PARAM(MANIFEST, manifest);
|
||||
FUNCTION_LOG_PARAM(STORAGE_INFO, fileInfo);
|
||||
FUNCTION_LOG_END();
|
||||
|
||||
// Get PostgreSQL version to write recovery for
|
||||
const unsigned int pgVersion = manifestData(manifest)->pgVersion;
|
||||
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// If recovery type is preserve then leave recovery file as it is
|
||||
if (cfgOptionSeq(cfgOptType) == CFGOPTVAL_RESTORE_TYPE_PRESERVE)
|
||||
{
|
||||
// Determine which file recovery settings will be written to
|
||||
const String *const recoveryFile =
|
||||
pgVersion >= PG_VERSION_RECOVERY_GUC ? PG_FILE_POSTGRESQLAUTOCONF_STR : PG_FILE_RECOVERYCONF_STR;
|
||||
|
||||
if (!storageExistsP(storagePg(), recoveryFile))
|
||||
{
|
||||
LOG_WARN_FMT(
|
||||
"recovery type is " CFGOPTVAL_RESTORE_TYPE_PRESERVE_Z " but recovery file does not exist at '%s'",
|
||||
strZ(storagePathP(storagePg(), recoveryFile)));
|
||||
}
|
||||
}
|
||||
// Else write recovery file
|
||||
else
|
||||
{
|
||||
// Generate a label used to identify this restore in the recovery file
|
||||
const String *const restoreLabel = strNewTimeP("%Y-%m-%d %H:%M:%S", time(NULL));
|
||||
|
||||
// Write recovery file based on PostgreSQL version
|
||||
if (pgVersion >= PG_VERSION_RECOVERY_GUC)
|
||||
restoreRecoveryWriteAutoConf(manifest, fileInfo, pgVersion, restoreLabel);
|
||||
else
|
||||
restoreRecoveryWriteConf(manifest, fileInfo, pgVersion, restoreLabel);
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
FUNCTION_LOG_RETURN_VOID();
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
/***********************************************************************************************************************************
|
||||
Generate a list of queues that determine the order of file processing
|
||||
***********************************************************************************************************************************/
|
||||
// Comparator to order ManifestFile objects by size then name
|
||||
static const Manifest *restoreProcessQueueComparatorManifest = NULL;
|
||||
|
||||
static int
|
||||
restoreProcessQueueComparator(const void *const item1, const void *const item2)
|
||||
{
|
||||
FUNCTION_TEST_BEGIN();
|
||||
FUNCTION_TEST_PARAM_P(VOID, item1);
|
||||
FUNCTION_TEST_PARAM_P(VOID, item2);
|
||||
FUNCTION_TEST_END();
|
||||
|
||||
ASSERT(item1 != NULL);
|
||||
ASSERT(item2 != NULL);
|
||||
|
||||
// Unpack files
|
||||
const ManifestFile file1 = manifestFileUnpack(restoreProcessQueueComparatorManifest, *(const ManifestFilePack *const *)item1);
|
||||
const ManifestFile file2 = manifestFileUnpack(restoreProcessQueueComparatorManifest, *(const ManifestFilePack *const *)item2);
|
||||
|
||||
// Zero length files should be ordered at the end
|
||||
if (file1.size == 0)
|
||||
{
|
||||
if (file2.size != 0)
|
||||
FUNCTION_TEST_RETURN(INT, -1);
|
||||
}
|
||||
else if (file2.size == 0)
|
||||
FUNCTION_TEST_RETURN(INT, 1);
|
||||
|
||||
// If the bundle id differs that is enough to determine order
|
||||
if (file1.bundleId < file2.bundleId)
|
||||
FUNCTION_TEST_RETURN(INT, 1);
|
||||
else if (file1.bundleId > file2.bundleId)
|
||||
FUNCTION_TEST_RETURN(INT, -1);
|
||||
|
||||
// If the bundle ids are 0
|
||||
if (file1.bundleId == 0)
|
||||
{
|
||||
// If the size differs then that's enough to determine order
|
||||
if (file1.size < file2.size)
|
||||
FUNCTION_TEST_RETURN(INT, -1);
|
||||
else if (file1.size > file2.size)
|
||||
FUNCTION_TEST_RETURN(INT, 1);
|
||||
|
||||
// If size is the same then use name to generate a deterministic ordering (names must be unique)
|
||||
ASSERT(!strEq(file1.name, file2.name));
|
||||
FUNCTION_TEST_RETURN(INT, strCmp(file1.name, file2.name));
|
||||
}
|
||||
|
||||
// If the reference differs that is enough to determine order
|
||||
if (file1.reference == NULL)
|
||||
{
|
||||
if (file2.reference != NULL)
|
||||
FUNCTION_TEST_RETURN(INT, -1);
|
||||
}
|
||||
else if (file2.reference == NULL)
|
||||
FUNCTION_TEST_RETURN(INT, 1);
|
||||
else
|
||||
{
|
||||
const int backupLabelCmp = strCmp(file1.reference, file2.reference) * -1;
|
||||
|
||||
if (backupLabelCmp != 0)
|
||||
FUNCTION_TEST_RETURN(INT, backupLabelCmp);
|
||||
}
|
||||
|
||||
// Finally order by bundle offset
|
||||
ASSERT(file1.bundleOffset != file2.bundleOffset);
|
||||
|
||||
if (file1.bundleOffset < file2.bundleOffset)
|
||||
FUNCTION_TEST_RETURN(INT, 1);
|
||||
|
||||
FUNCTION_TEST_RETURN(INT, -1);
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
restoreProcessQueue(const Manifest *const manifest, List **const queueList)
|
||||
{
|
||||
FUNCTION_LOG_BEGIN(logLevelDebug);
|
||||
FUNCTION_LOG_PARAM(MANIFEST, manifest);
|
||||
FUNCTION_LOG_PARAM_P(LIST, queueList);
|
||||
FUNCTION_LOG_END();
|
||||
|
||||
FUNCTION_AUDIT_HELPER();
|
||||
|
||||
ASSERT(manifest != NULL);
|
||||
|
||||
uint64_t result = 0;
|
||||
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// Create list of process queues (use void * instead of List * to avoid Coverity false positive)
|
||||
*queueList = lstNewP(sizeof(void *));
|
||||
|
||||
// Generate the list of processing queues (there is always at least one)
|
||||
StringList *const targetList = strLstNew();
|
||||
strLstAddZ(targetList, MANIFEST_TARGET_PGDATA "/");
|
||||
|
||||
for (unsigned int targetIdx = 0; targetIdx < manifestTargetTotal(manifest); targetIdx++)
|
||||
{
|
||||
const ManifestTarget *const target = manifestTarget(manifest, targetIdx);
|
||||
|
||||
if (target->tablespaceId != 0)
|
||||
strLstAddFmt(targetList, "%s/", strZ(target->name));
|
||||
}
|
||||
|
||||
// Generate the processing queues
|
||||
MEM_CONTEXT_BEGIN(lstMemContext(*queueList))
|
||||
{
|
||||
for (unsigned int targetIdx = 0; targetIdx < strLstSize(targetList); targetIdx++)
|
||||
{
|
||||
List *const queue = lstNewP(sizeof(ManifestFile *), .comparator = restoreProcessQueueComparator);
|
||||
lstAdd(*queueList, &queue);
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_END();
|
||||
|
||||
// Now put all files into the processing queues
|
||||
for (unsigned int fileIdx = 0; fileIdx < manifestFileTotal(manifest); fileIdx++)
|
||||
{
|
||||
const ManifestFilePack *const filePack = manifestFilePackGet(manifest, fileIdx);
|
||||
const ManifestFile file = manifestFileUnpack(manifest, filePack);
|
||||
|
||||
// Find the target that contains this file
|
||||
unsigned int targetIdx = 0;
|
||||
|
||||
do
|
||||
{
|
||||
// A target should always be found
|
||||
CHECK(FormatError, targetIdx < strLstSize(targetList), "backup target not found");
|
||||
|
||||
if (strBeginsWith(file.name, strLstGet(targetList, targetIdx)))
|
||||
break;
|
||||
|
||||
targetIdx++;
|
||||
}
|
||||
while (1);
|
||||
|
||||
// Add file to queue
|
||||
lstAdd(*(List **)lstGet(*queueList, targetIdx), &filePack);
|
||||
|
||||
// Add size to total
|
||||
result += file.size;
|
||||
}
|
||||
|
||||
// Sort the queues
|
||||
restoreProcessQueueComparatorManifest = manifest;
|
||||
|
||||
for (unsigned int targetIdx = 0; targetIdx < strLstSize(targetList); targetIdx++)
|
||||
lstSort(*(List **)lstGet(*queueList, targetIdx), sortOrderDesc);
|
||||
|
||||
// Move process queues to prior context
|
||||
lstMove(*queueList, memContextPrior());
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
FUNCTION_LOG_RETURN(UINT64, result);
|
||||
}
|
||||
|
||||
/***********************************************************************************************************************************
|
||||
Log the results of a job and throw errors
|
||||
***********************************************************************************************************************************/
|
||||
// Helper function to determine if a file should be zeroed
|
||||
static bool
|
||||
restoreFileZeroed(const String *const manifestName, RegExp *const zeroExp)
|
||||
{
|
||||
FUNCTION_TEST_BEGIN();
|
||||
FUNCTION_TEST_PARAM(STRING, manifestName);
|
||||
FUNCTION_TEST_PARAM(REGEXP, zeroExp);
|
||||
FUNCTION_TEST_END();
|
||||
|
||||
ASSERT(manifestName != NULL);
|
||||
|
||||
FUNCTION_TEST_RETURN(
|
||||
BOOL,
|
||||
zeroExp == NULL ? false : regExpMatch(zeroExp, manifestName) && !strEndsWith(manifestName, STRDEF("/" PG_FILE_PGVERSION)));
|
||||
}
|
||||
|
||||
// Helper function to construct the absolute pg path for any file. Add a temp extension to pg_control so a partially restored
|
||||
// cluster cannot be started.
|
||||
static String *
|
||||
restoreFilePgPath(const Manifest *const manifest, const String *const manifestName)
|
||||
{
|
||||
FUNCTION_TEST_BEGIN();
|
||||
FUNCTION_TEST_PARAM(MANIFEST, manifest);
|
||||
FUNCTION_TEST_PARAM(STRING, manifestName);
|
||||
FUNCTION_TEST_END();
|
||||
|
||||
ASSERT(manifest != NULL);
|
||||
ASSERT(manifestName != NULL);
|
||||
|
||||
String *const pathPg = manifestPathPg(manifestName);
|
||||
String *const result = strNewFmt(
|
||||
"%s/%s%s", strZ(manifestTargetBase(manifest)->path), strZ(pathPg),
|
||||
strEqZ(manifestName, MANIFEST_TARGET_PGDATA "/" PG_PATH_GLOBAL "/" PG_FILE_PGCONTROL) ? "." STORAGE_FILE_TEMP_EXT : "");
|
||||
|
||||
strFree(pathPg);
|
||||
|
||||
FUNCTION_TEST_RETURN(STRING, result);
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
restoreJobResult(
|
||||
const Manifest *const manifest, ProtocolParallelJob *const job, RegExp *const zeroExp, const uint64_t sizeTotal,
|
||||
uint64_t sizeRestored, unsigned int *const currentPercentComplete)
|
||||
{
|
||||
FUNCTION_LOG_BEGIN(logLevelDebug);
|
||||
FUNCTION_LOG_PARAM(MANIFEST, manifest);
|
||||
FUNCTION_LOG_PARAM(PROTOCOL_PARALLEL_JOB, job);
|
||||
FUNCTION_LOG_PARAM(REGEXP, zeroExp);
|
||||
FUNCTION_LOG_PARAM(UINT64, sizeTotal);
|
||||
FUNCTION_LOG_PARAM(UINT64, sizeRestored);
|
||||
FUNCTION_LOG_PARAM_P(UINT, currentPercentComplete);
|
||||
FUNCTION_LOG_END();
|
||||
|
||||
ASSERT(manifest != NULL);
|
||||
|
||||
// The job was successful
|
||||
if (protocolParallelJobErrorCode(job) == 0)
|
||||
{
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
PackRead *const jobResult = protocolParallelJobResult(job);
|
||||
unsigned int percentComplete = 0;
|
||||
|
||||
while (!pckReadNullP(jobResult))
|
||||
{
|
||||
const ManifestFile file = manifestFileFind(manifest, pckReadStrP(jobResult));
|
||||
const bool zeroed = restoreFileZeroed(file.name, zeroExp);
|
||||
const RestoreResult result = (RestoreResult)pckReadU32P(jobResult);
|
||||
const uint64_t blockIncrDeltaSize = pckReadU64P(jobResult);
|
||||
|
||||
String *const log = strCatZ(strNew(), "restore");
|
||||
|
||||
// Note if file was zeroed (i.e. selective restore)
|
||||
if (zeroed)
|
||||
strCatZ(log, " zeroed");
|
||||
|
||||
// Add filename
|
||||
strCatFmt(log, " file %s", strZ(restoreFilePgPath(manifest, file.name)));
|
||||
|
||||
// If preserved add details to explain why it was not copied or zeroed
|
||||
if (result == restoreResultPreserve)
|
||||
{
|
||||
strCatZ(log, " - ");
|
||||
|
||||
// On force we match on size and modification time
|
||||
if (cfgOptionBool(cfgOptForce))
|
||||
{
|
||||
strCatFmt(
|
||||
log, "exists and matches size %" PRIu64 " and modification time %" PRIu64, file.size,
|
||||
(uint64_t)file.timestamp);
|
||||
}
|
||||
// Else a checksum delta or file is zero-length
|
||||
else
|
||||
{
|
||||
strCatZ(log, "exists and ");
|
||||
|
||||
// No need to copy zero-length files
|
||||
if (file.size == 0)
|
||||
{
|
||||
strCatZ(log, "is zero size");
|
||||
}
|
||||
// The file matched the manifest checksum so did not need to be copied
|
||||
else
|
||||
strCatZ(log, "matches backup");
|
||||
}
|
||||
}
|
||||
|
||||
// Add bundle info
|
||||
strCatZ(log, " (");
|
||||
|
||||
if (file.bundleId != 0)
|
||||
{
|
||||
ASSERT(varUInt64(protocolParallelJobKey(job)) == file.bundleId);
|
||||
|
||||
strCatZ(log, "bundle ");
|
||||
|
||||
if (file.reference != NULL)
|
||||
strCatFmt(log, "%s/", strZ(file.reference));
|
||||
|
||||
strCatFmt(log, "%" PRIu64 "/%" PRIu64 ", ", file.bundleId, file.bundleOffset);
|
||||
}
|
||||
|
||||
// Add block incremental delta size, i.e. amount of the file that block incremental updated
|
||||
if (file.blockIncrMapSize != 0 && result != restoreResultPreserve)
|
||||
{
|
||||
strCatZ(log, "bi ");
|
||||
|
||||
if (blockIncrDeltaSize != file.size)
|
||||
strCatFmt(log, "%s/", strZ(strSizeFormat(blockIncrDeltaSize)));
|
||||
}
|
||||
|
||||
// Add size and percent complete
|
||||
sizeRestored += file.size;
|
||||
|
||||
// Store percentComplete as an integer (used to update progress in the lock file)
|
||||
percentComplete = cvtPctToUInt(sizeRestored, sizeTotal);
|
||||
|
||||
strCatFmt(log, "%s, %s)", strZ(strSizeFormat(file.size)), strZ(strNewPct(sizeRestored, sizeTotal)));
|
||||
|
||||
// If not zero-length add the checksum
|
||||
if (file.size != 0 && !zeroed)
|
||||
strCatFmt(log, " checksum %s", strZ(strNewEncode(encodingHex, BUF(file.checksumSha1, HASH_TYPE_SHA1_SIZE))));
|
||||
|
||||
LOG_DETAIL_PID(protocolParallelJobProcessId(job), strZ(log));
|
||||
}
|
||||
|
||||
// Update currentPercentComplete and lock file when the change is significant enough
|
||||
if (percentComplete - *currentPercentComplete > 10)
|
||||
{
|
||||
*currentPercentComplete = percentComplete;
|
||||
cmdLockWriteP(
|
||||
.percentComplete = VARUINT(*currentPercentComplete), .sizeComplete = VARUINT64(sizeRestored),
|
||||
.size = VARUINT64(sizeTotal));
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
// Free the job
|
||||
protocolParallelJobFree(job);
|
||||
}
|
||||
// Else the job errored
|
||||
else
|
||||
THROW_CODE(protocolParallelJobErrorCode(job), strZ(protocolParallelJobErrorMessage(job)));
|
||||
|
||||
FUNCTION_LOG_RETURN(UINT64, sizeRestored);
|
||||
}
|
||||
|
||||
/***********************************************************************************************************************************
|
||||
Return new restore jobs as requested
|
||||
***********************************************************************************************************************************/
|
||||
typedef struct RestoreJobData
|
||||
{
|
||||
unsigned int repoIdx; // Internal repo idx
|
||||
Manifest *manifest; // Backup manifest
|
||||
List *queueList; // List of processing queues
|
||||
RegExp *zeroExp; // Identify files that should be sparse zeroed
|
||||
const String *cipherSubPass; // Passphrase used to decrypt files in the backup
|
||||
const String *rootReplaceUser; // User to replace invalid users when root
|
||||
const String *rootReplaceGroup; // Group to replace invalid group when root
|
||||
} RestoreJobData;
|
||||
|
||||
// Helper to calculate the next queue to scan based on the client index
|
||||
static int
|
||||
restoreJobQueueNext(const unsigned int clientIdx, int queueIdx, const unsigned int queueTotal)
|
||||
{
|
||||
FUNCTION_TEST_BEGIN();
|
||||
FUNCTION_TEST_PARAM(UINT, clientIdx);
|
||||
FUNCTION_TEST_PARAM(INT, queueIdx);
|
||||
FUNCTION_TEST_PARAM(UINT, queueTotal);
|
||||
FUNCTION_TEST_END();
|
||||
|
||||
// Move (forward or back) to the next queue
|
||||
queueIdx += clientIdx % 2 ? -1 : 1;
|
||||
|
||||
// Deal with wrapping on either end
|
||||
if (queueIdx < 0)
|
||||
FUNCTION_TEST_RETURN(INT, (int)queueTotal - 1);
|
||||
else if (queueIdx == (int)queueTotal)
|
||||
FUNCTION_TEST_RETURN(INT, 0);
|
||||
|
||||
FUNCTION_TEST_RETURN(INT, queueIdx);
|
||||
}
|
||||
|
||||
// Callback to fetch restore jobs for the parallel executor
|
||||
static ProtocolParallelJob *
|
||||
restoreJobCallback(void *const data, const unsigned int clientIdx)
|
||||
{
|
||||
FUNCTION_TEST_BEGIN();
|
||||
FUNCTION_TEST_PARAM_P(VOID, data);
|
||||
FUNCTION_TEST_PARAM(UINT, clientIdx);
|
||||
FUNCTION_TEST_END();
|
||||
|
||||
ASSERT(data != NULL);
|
||||
|
||||
ProtocolParallelJob *result = NULL;
|
||||
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// Get a new job if there are any left
|
||||
RestoreJobData *const jobData = data;
|
||||
|
||||
// Determine where to begin scanning the queue (we'll stop when we get back here)
|
||||
PackWrite *param = NULL;
|
||||
int queueIdx = (int)(clientIdx % lstSize(jobData->queueList));
|
||||
const int queueEnd = queueIdx;
|
||||
|
||||
// Create restore job
|
||||
do
|
||||
{
|
||||
List *const queue = *(List **)lstGet(jobData->queueList, (unsigned int)queueIdx);
|
||||
bool fileAdded = false;
|
||||
const String *fileName = NULL;
|
||||
uint64_t bundleId = 0;
|
||||
const String *reference = NULL;
|
||||
|
||||
while (!lstEmpty(queue))
|
||||
{
|
||||
const ManifestFile file = manifestFileUnpack(jobData->manifest, *(ManifestFilePack **)lstGet(queue, 0));
|
||||
|
||||
// Break if bundled files have already been added and 1) the bundleId has changed or 2) the reference has changed
|
||||
if (fileAdded && (bundleId != file.bundleId || !strEq(reference, file.reference)))
|
||||
break;
|
||||
|
||||
// Add common parameters before first file
|
||||
if (param == NULL)
|
||||
{
|
||||
param = protocolPackNew();
|
||||
|
||||
if (file.bundleId != 0)
|
||||
{
|
||||
bundleId = file.bundleId;
|
||||
reference = file.reference;
|
||||
}
|
||||
else
|
||||
fileName = file.name;
|
||||
|
||||
pckWriteStrP(
|
||||
param,
|
||||
backupFileRepoPathP(
|
||||
file.reference != NULL ? file.reference : manifestData(jobData->manifest)->backupLabel,
|
||||
.manifestName = file.name, .bundleId = file.bundleId,
|
||||
.compressType = manifestData(jobData->manifest)->backupOptionCompressType,
|
||||
.blockIncr = file.blockIncrMapSize != 0));
|
||||
pckWriteU32P(param, jobData->repoIdx);
|
||||
pckWriteU32P(param, manifestData(jobData->manifest)->backupOptionCompressType);
|
||||
pckWriteTimeP(param, manifestData(jobData->manifest)->backupTimestampCopyStart);
|
||||
pckWriteBoolP(param, cfgOptionBool(cfgOptDelta));
|
||||
pckWriteBoolP(param, cfgOptionBool(cfgOptDelta) && cfgOptionBool(cfgOptForce));
|
||||
pckWriteBoolP(param, file.bundleId != 0 && manifestData(jobData->manifest)->bundleRaw);
|
||||
pckWriteStrP(param, jobData->cipherSubPass);
|
||||
pckWriteStrLstP(param, manifestReferenceList(jobData->manifest));
|
||||
|
||||
fileAdded = true;
|
||||
}
|
||||
|
||||
pckWriteStrP(param, restoreFilePgPath(jobData->manifest, file.name));
|
||||
pckWriteBinP(param, BUF(file.checksumSha1, HASH_TYPE_SHA1_SIZE));
|
||||
pckWriteU64P(param, file.size);
|
||||
pckWriteTimeP(param, file.timestamp);
|
||||
pckWriteModeP(param, file.mode);
|
||||
pckWriteBoolP(param, restoreFileZeroed(file.name, jobData->zeroExp));
|
||||
pckWriteStrP(param, restoreManifestOwnerReplace(file.user, jobData->rootReplaceUser));
|
||||
pckWriteStrP(param, restoreManifestOwnerReplace(file.group, jobData->rootReplaceGroup));
|
||||
|
||||
// If block incremental then modify offset and size to where the map is stored since we need to read that first.
|
||||
if (file.blockIncrMapSize != 0)
|
||||
{
|
||||
pckWriteBoolP(param, true);
|
||||
pckWriteU64P(param, file.bundleOffset + file.sizeRepo - file.blockIncrMapSize);
|
||||
pckWriteU64P(param, file.blockIncrMapSize);
|
||||
}
|
||||
// Else write bundle offset/size
|
||||
else if (file.bundleId != 0)
|
||||
{
|
||||
pckWriteBoolP(param, true);
|
||||
pckWriteU64P(param, file.bundleOffset);
|
||||
pckWriteU64P(param, file.sizeRepo);
|
||||
}
|
||||
// Else restore as a whole file
|
||||
else
|
||||
pckWriteBoolP(param, false);
|
||||
|
||||
// Block incremental
|
||||
pckWriteU64P(param, file.blockIncrMapSize);
|
||||
|
||||
if (file.blockIncrMapSize != 0)
|
||||
{
|
||||
pckWriteU64P(param, file.blockIncrSize);
|
||||
pckWriteU64P(param, file.blockIncrChecksumSize);
|
||||
}
|
||||
|
||||
pckWriteStrP(param, file.name);
|
||||
|
||||
// Remove job from the queue
|
||||
lstRemoveIdx(queue, 0);
|
||||
|
||||
// Break if the file is not bundled
|
||||
if (bundleId == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
if (fileAdded)
|
||||
{
|
||||
// Assign job to result
|
||||
MEM_CONTEXT_PRIOR_BEGIN()
|
||||
{
|
||||
result = protocolParallelJobNew(
|
||||
bundleId != 0 ? VARUINT64(bundleId) : VARSTR(fileName), PROTOCOL_COMMAND_RESTORE_FILE, param);
|
||||
}
|
||||
MEM_CONTEXT_PRIOR_END();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
queueIdx = restoreJobQueueNext(clientIdx, queueIdx, lstSize(jobData->queueList));
|
||||
}
|
||||
while (queueIdx != queueEnd);
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
FUNCTION_TEST_RETURN(PROTOCOL_PARALLEL_JOB, result);
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
/***********************************************************************************************************************************
|
||||
Remap the manifest based on mappings provided by the user
|
||||
***********************************************************************************************************************************/
|
||||
static void
|
||||
restoreManifestMap(Manifest *const manifest)
|
||||
{
|
||||
FUNCTION_LOG_BEGIN(logLevelDebug);
|
||||
FUNCTION_LOG_PARAM(MANIFEST, manifest);
|
||||
FUNCTION_LOG_END();
|
||||
|
||||
ASSERT(manifest != NULL);
|
||||
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// Remap the data directory
|
||||
// -------------------------------------------------------------------------------------------------------------------------
|
||||
const String *const pgPath = cfgOptionStr(cfgOptPgPath);
|
||||
const ManifestTarget *const targetBase = manifestTargetBase(manifest);
|
||||
|
||||
if (!strEq(targetBase->path, pgPath))
|
||||
{
|
||||
LOG_INFO_FMT("remap data directory to '%s'", strZ(pgPath));
|
||||
manifestTargetUpdate(manifest, targetBase->name, pgPath, NULL);
|
||||
}
|
||||
|
||||
// Remap tablespaces
|
||||
// -------------------------------------------------------------------------------------------------------------------------
|
||||
const KeyValue *const tablespaceMap = cfgOptionKvNull(cfgOptTablespaceMap);
|
||||
const String *const tablespaceMapAllPath = cfgOptionStrNull(cfgOptTablespaceMapAll);
|
||||
|
||||
if (tablespaceMap != NULL || tablespaceMapAllPath != NULL)
|
||||
{
|
||||
StringList *const tablespaceRemapped = strLstNew();
|
||||
|
||||
for (unsigned int targetIdx = 0; targetIdx < manifestTargetTotal(manifest); targetIdx++)
|
||||
{
|
||||
const ManifestTarget *const target = manifestTarget(manifest, targetIdx);
|
||||
|
||||
// Is this a tablespace?
|
||||
if (target->tablespaceId != 0)
|
||||
{
|
||||
const String *tablespacePath = NULL;
|
||||
|
||||
// Check for an individual mapping for this tablespace
|
||||
if (tablespaceMap != NULL)
|
||||
{
|
||||
// Attempt to get the tablespace by name
|
||||
const String *const tablespacePathByName = varStr(kvGet(tablespaceMap, VARSTR(target->tablespaceName)));
|
||||
|
||||
if (tablespacePathByName != NULL)
|
||||
strLstAdd(tablespaceRemapped, target->tablespaceName);
|
||||
|
||||
// Attempt to get the tablespace by id
|
||||
const String *const tablespacePathById = varStr(
|
||||
kvGet(tablespaceMap, VARSTR(varStrForce(VARUINT(target->tablespaceId)))));
|
||||
|
||||
if (tablespacePathById != NULL)
|
||||
strLstAdd(tablespaceRemapped, varStrForce(VARUINT(target->tablespaceId)));
|
||||
|
||||
// Error when both are set but the paths are different
|
||||
if (tablespacePathByName != NULL && tablespacePathById != NULL && !
|
||||
strEq(tablespacePathByName, tablespacePathById))
|
||||
{
|
||||
THROW_FMT(
|
||||
TablespaceMapError, "tablespace remapped by name '%s' and id %u with different paths",
|
||||
strZ(target->tablespaceName), target->tablespaceId);
|
||||
}
|
||||
// Else set the path by name
|
||||
else if (tablespacePathByName != NULL)
|
||||
{
|
||||
tablespacePath = tablespacePathByName;
|
||||
}
|
||||
// Else set the path by id
|
||||
else if (tablespacePathById != NULL)
|
||||
tablespacePath = tablespacePathById;
|
||||
}
|
||||
|
||||
// If not individual mapping check if all tablespaces are being remapped
|
||||
if (tablespacePath == NULL && tablespaceMapAllPath != NULL)
|
||||
tablespacePath = strNewFmt("%s/%s", strZ(tablespaceMapAllPath), strZ(target->tablespaceName));
|
||||
|
||||
// Remap tablespace if a mapping was found
|
||||
if (tablespacePath != NULL)
|
||||
{
|
||||
LOG_INFO_FMT("map tablespace '%s' to '%s'", strZ(target->name), strZ(tablespacePath));
|
||||
|
||||
manifestTargetUpdate(manifest, target->name, tablespacePath, NULL);
|
||||
manifestLinkUpdate(manifest, strNewFmt(MANIFEST_TARGET_PGDATA "/%s", strZ(target->name)), tablespacePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error on invalid tablespaces
|
||||
if (tablespaceMap != NULL)
|
||||
{
|
||||
const VariantList *const tablespaceMapList = kvKeyList(tablespaceMap);
|
||||
strLstSort(tablespaceRemapped, sortOrderAsc);
|
||||
|
||||
for (unsigned int tablespaceMapIdx = 0; tablespaceMapIdx < varLstSize(tablespaceMapList); tablespaceMapIdx++)
|
||||
{
|
||||
const String *const tablespace = varStr(varLstGet(tablespaceMapList, tablespaceMapIdx));
|
||||
|
||||
if (!strLstExists(tablespaceRemapped, tablespace))
|
||||
THROW_FMT(TablespaceMapError, "unable to remap invalid tablespace '%s'", strZ(tablespace));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remap links
|
||||
// -------------------------------------------------------------------------------------------------------------------------
|
||||
const KeyValue *const linkMap = cfgOptionKvNull(cfgOptLinkMap);
|
||||
|
||||
if (linkMap != NULL)
|
||||
{
|
||||
const StringList *const linkMapList = strLstSort(strLstNewVarLst(kvKeyList(linkMap)), sortOrderAsc);
|
||||
|
||||
for (unsigned int linkMapIdx = 0; linkMapIdx < strLstSize(linkMapList); linkMapIdx++)
|
||||
{
|
||||
const String *const link = strLstGet(linkMapList, linkMapIdx);
|
||||
const String *const linkPath = varStr(kvGet(linkMap, VARSTR(link)));
|
||||
const String *const manifestName = strNewFmt(MANIFEST_TARGET_PGDATA "/%s", strZ(link));
|
||||
|
||||
// Attempt to find the link target
|
||||
ManifestTarget target = {0};
|
||||
|
||||
if (manifestTargetFindDefault(manifest, manifestName, NULL) != NULL)
|
||||
target = *manifestTargetFind(manifest, manifestName);
|
||||
|
||||
// If the target was not found then check if the link is a valid file or path
|
||||
bool create = false;
|
||||
|
||||
if (target.name == NULL)
|
||||
{
|
||||
// Is the specified link a file or a path? Error if they both match.
|
||||
const bool pathExists = manifestPathFindDefault(manifest, manifestName, NULL) != NULL;
|
||||
const bool fileExists = manifestFileExists(manifest, manifestName);
|
||||
|
||||
CHECK(FormatError, !pathExists || !fileExists, "link may not be both file and path");
|
||||
|
||||
target = (ManifestTarget){.name = manifestName, .path = linkPath, .type = manifestTargetTypeLink};
|
||||
|
||||
// If a file
|
||||
if (fileExists)
|
||||
{
|
||||
// File needs to be set so the file/path is updated later but set it to something invalid just in case it
|
||||
// it does not get updated due to a regression
|
||||
target.file = DOT_STR;
|
||||
}
|
||||
// Else error if not a path
|
||||
else if (!pathExists)
|
||||
{
|
||||
THROW_FMT(
|
||||
LinkMapError,
|
||||
"unable to map link '%s'\n"
|
||||
"HINT: Does the link reference a valid backup path or file?",
|
||||
strZ(link));
|
||||
}
|
||||
|
||||
// Add the link. Copy user/group from the base data directory.
|
||||
const ManifestPath *const pathBase = manifestPathFind(manifest, MANIFEST_TARGET_PGDATA_STR);
|
||||
const ManifestLink manifestLink =
|
||||
{
|
||||
.name = manifestName,
|
||||
.destination = linkPath,
|
||||
.group = pathBase->group,
|
||||
.user = pathBase->user,
|
||||
};
|
||||
|
||||
manifestLinkAdd(manifest, &manifestLink);
|
||||
create = true;
|
||||
}
|
||||
// Else update target to new path
|
||||
else
|
||||
target.path = linkPath;
|
||||
|
||||
// The target must be a link since pg_data/ was prepended and pgdata is the only allowed path
|
||||
CHECK(FormatError, target.type == manifestTargetTypeLink, "target must be a link");
|
||||
|
||||
// Error if the target is a tablespace
|
||||
if (target.tablespaceId != 0)
|
||||
{
|
||||
THROW_FMT(
|
||||
LinkMapError,
|
||||
"unable to remap tablespace '%s'\n"
|
||||
"HINT: use '" CFGOPT_TABLESPACE_MAP "' option to remap tablespaces.",
|
||||
strZ(link));
|
||||
}
|
||||
|
||||
LOG_INFO_FMT("%slink '%s' to '%s'", create ? "" : "map ", strZ(link), strZ(target.path));
|
||||
|
||||
// If the link was not created update to the new destination
|
||||
if (!create)
|
||||
manifestLinkUpdate(manifest, target.name, target.path);
|
||||
|
||||
// If the link is a file separate the file name from the path
|
||||
if (target.file != NULL)
|
||||
{
|
||||
// The link destination must have at least one path component in addition to the file part. So '..' would
|
||||
// not be a valid destination but '../file' or '/file' is.
|
||||
if (strSize(strPath(target.path)) == 0)
|
||||
{
|
||||
THROW_FMT(
|
||||
LinkMapError, "'%s' is not long enough to be the destination for file link '%s'", strZ(target.path),
|
||||
strZ(link));
|
||||
}
|
||||
|
||||
target.file = strBase(target.path);
|
||||
target.path = strPath(target.path);
|
||||
}
|
||||
|
||||
// Create a new target or update the existing target file/path
|
||||
if (create)
|
||||
manifestTargetAdd(manifest, &target);
|
||||
else
|
||||
manifestTargetUpdate(manifest, target.name, target.path, target.file);
|
||||
}
|
||||
}
|
||||
|
||||
// If all links are not being restored then check for links that were not remapped and remove them
|
||||
if (!cfgOptionBool(cfgOptLinkAll))
|
||||
{
|
||||
unsigned int targetIdx = 0;
|
||||
|
||||
while (targetIdx < manifestTargetTotal(manifest))
|
||||
{
|
||||
const ManifestTarget *const target = manifestTarget(manifest, targetIdx);
|
||||
|
||||
// Is this a non-tablespace link?
|
||||
if (target->type == manifestTargetTypeLink && target->tablespaceId == 0)
|
||||
{
|
||||
const String *const link = strSub(target->name, strSize(MANIFEST_TARGET_PGDATA_STR) + 1);
|
||||
|
||||
// If the link was not remapped then remove it
|
||||
if (linkMap == NULL || kvGet(linkMap, VARSTR(link)) == NULL)
|
||||
{
|
||||
if (target->file != NULL)
|
||||
LOG_WARN_FMT("file link '%s' will be restored as a file at the same location", strZ(link));
|
||||
else
|
||||
{
|
||||
LOG_WARN_FMT(
|
||||
"contents of directory link '%s' will be restored in a directory at the same location", strZ(link));
|
||||
}
|
||||
|
||||
manifestLinkRemove(manifest, target->name);
|
||||
manifestTargetRemove(manifest, target->name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
targetIdx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
FUNCTION_LOG_RETURN_VOID();
|
||||
}
|
||||
+6
-2329
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,427 @@
|
||||
/***********************************************************************************************************************************
|
||||
Get the backup set to restore
|
||||
***********************************************************************************************************************************/
|
||||
typedef struct RestoreBackupData
|
||||
{
|
||||
unsigned int repoIdx; // Internal repo index
|
||||
CipherType repoCipherType; // Repo encryption type (0 = none)
|
||||
const String *backupCipherPass; // Passphrase of backup files if repo is encrypted (else NULL)
|
||||
const String *backupSet; // Backup set to restore
|
||||
} RestoreBackupData;
|
||||
|
||||
#define FUNCTION_LOG_RESTORE_BACKUP_DATA_TYPE \
|
||||
RestoreBackupData
|
||||
#define FUNCTION_LOG_RESTORE_BACKUP_DATA_FORMAT(value, buffer, bufferSize) \
|
||||
objNameToLog(&value, "RestoreBackupData", buffer, bufferSize)
|
||||
|
||||
// Helper function for restoreBackupSet
|
||||
static RestoreBackupData
|
||||
restoreBackupData(const String *const backupLabel, const unsigned int repoIdx, const String *const backupCipherPass)
|
||||
{
|
||||
ASSERT(backupLabel != NULL);
|
||||
|
||||
RestoreBackupData restoreBackup = {0};
|
||||
|
||||
MEM_CONTEXT_PRIOR_BEGIN()
|
||||
{
|
||||
restoreBackup.backupSet = strDup(backupLabel);
|
||||
restoreBackup.repoIdx = repoIdx;
|
||||
restoreBackup.repoCipherType = cfgOptionIdxStrId(cfgOptRepoCipherType, repoIdx);
|
||||
restoreBackup.backupCipherPass = strDup(backupCipherPass);
|
||||
}
|
||||
MEM_CONTEXT_PRIOR_END();
|
||||
|
||||
return restoreBackup;
|
||||
}
|
||||
|
||||
static RestoreBackupData
|
||||
restoreBackupSet(void)
|
||||
{
|
||||
FUNCTION_LOG_VOID(logLevelDebug);
|
||||
|
||||
FUNCTION_AUDIT_STRUCT();
|
||||
|
||||
RestoreBackupData result = {0};
|
||||
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// Initialize the repo index
|
||||
unsigned int repoIdxMin = 0;
|
||||
unsigned int repoIdxMax = cfgOptionGroupIdxTotal(cfgOptGrpRepo) - 1;
|
||||
|
||||
// If the repo was specified then set index to the array location and max to loop only once
|
||||
if (cfgOptionTest(cfgOptRepo))
|
||||
{
|
||||
repoIdxMin = cfgOptionGroupIdxDefault(cfgOptGrpRepo);
|
||||
repoIdxMax = repoIdxMin;
|
||||
}
|
||||
|
||||
// If the set option was not provided by the user but a target was set, then we will need to search for a backup set that
|
||||
// satisfies the target condition, else we will use the backup provided
|
||||
const String *backupSetRequested = NULL;
|
||||
const unsigned int targetType = cfgOptionSeq(cfgOptType);
|
||||
|
||||
union
|
||||
{
|
||||
time_t time;
|
||||
uint64_t lsn;
|
||||
} target = {0};
|
||||
|
||||
if (cfgOptionSource(cfgOptSet) == cfgSourceDefault)
|
||||
{
|
||||
if (targetType == CFGOPTVAL_RESTORE_TYPE_TIME)
|
||||
{
|
||||
TRY_BEGIN()
|
||||
{
|
||||
target.time = cvtZToTime(strZ(cfgOptionStr(cfgOptTarget)));
|
||||
}
|
||||
CATCH_ANY()
|
||||
{
|
||||
THROW_FMT(
|
||||
FormatError,
|
||||
"automatic backup set selection cannot be performed with provided time '%s'\n"
|
||||
"HINT: time format must be YYYY-MM-DD HH:MM:SS with optional msec and optional timezone (+/- HH or HHMM or"
|
||||
" HH:MM) - if timezone is omitted, local time is assumed (for UTC use +00)",
|
||||
strZ(cfgOptionStr(cfgOptTarget)));
|
||||
}
|
||||
TRY_END();
|
||||
}
|
||||
else if (targetType == CFGOPTVAL_RESTORE_TYPE_LSN)
|
||||
target.lsn = pgLsnFromStr(cfgOptionStr(cfgOptTarget));
|
||||
}
|
||||
else
|
||||
backupSetRequested = cfgOptionStr(cfgOptSet);
|
||||
|
||||
// Search through the repo list for a backup set to use for recovery
|
||||
for (unsigned int repoIdx = repoIdxMin; repoIdx <= repoIdxMax; repoIdx++)
|
||||
{
|
||||
// Get the repo storage in case it is remote and encryption settings need to be pulled down
|
||||
storageRepoIdx(repoIdx);
|
||||
|
||||
const InfoBackup *infoBackup = NULL;
|
||||
|
||||
// Attempt to load backup.info
|
||||
TRY_BEGIN()
|
||||
{
|
||||
infoBackup = infoBackupLoadFile(
|
||||
storageRepoIdx(repoIdx), INFO_BACKUP_PATH_FILE_STR, cfgOptionIdxStrId(cfgOptRepoCipherType, repoIdx),
|
||||
cfgOptionIdxStrNull(cfgOptRepoCipherPass, repoIdx));
|
||||
}
|
||||
CATCH_ANY()
|
||||
{
|
||||
LOG_WARN_FMT("%s: [%s] %s", cfgOptionGroupName(cfgOptGrpRepo, repoIdx), errorTypeName(errorType()), errorMessage());
|
||||
}
|
||||
TRY_END();
|
||||
|
||||
// If unable to load the backup info file, then move on to next repo
|
||||
if (infoBackup == NULL)
|
||||
continue;
|
||||
|
||||
if (infoBackupDataTotal(infoBackup) == 0)
|
||||
{
|
||||
LOG_WARN_FMT(
|
||||
"%s: [%s] no backup sets to restore", cfgOptionGroupName(cfgOptGrpRepo, repoIdx),
|
||||
errorTypeName(&BackupSetInvalidError));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If a backup set was not specified, then see if a target was requested
|
||||
if (backupSetRequested == NULL)
|
||||
{
|
||||
// Get the latest backup
|
||||
const InfoBackupData latestBackup = infoBackupData(infoBackup, infoBackupDataTotal(infoBackup) - 1);
|
||||
|
||||
// If a target was requested, attempt to determine the backup set
|
||||
if (targetType == CFGOPTVAL_RESTORE_TYPE_TIME || targetType == CFGOPTVAL_RESTORE_TYPE_LSN)
|
||||
{
|
||||
bool found = false;
|
||||
|
||||
// Search current backups from newest to oldest
|
||||
for (unsigned int keyIdx = infoBackupDataTotal(infoBackup) - 1; (int)keyIdx >= 0; keyIdx--)
|
||||
{
|
||||
// Get the backup data
|
||||
const InfoBackupData backupData = infoBackupData(infoBackup, keyIdx);
|
||||
|
||||
// If target is lsn and no backupLsnStop exists, exit this repo and log that backup may be manually selected
|
||||
if (targetType == CFGOPTVAL_RESTORE_TYPE_LSN && !backupData.backupLsnStop)
|
||||
{
|
||||
LOG_WARN_FMT(
|
||||
"%s reached backup from prior version missing required LSN info before finding a match -- backup"
|
||||
" auto-select has been disabled for this repo\n"
|
||||
"HINT: you may specify a backup to restore using the --set option.",
|
||||
cfgOptionGroupName(cfgOptGrpRepo, repoIdx));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// If the end of the backup is valid for the target, then select this backup
|
||||
if ((targetType == CFGOPTVAL_RESTORE_TYPE_TIME && backupData.backupTimestampStop < target.time) ||
|
||||
(targetType == CFGOPTVAL_RESTORE_TYPE_LSN && pgLsnFromStr(backupData.backupLsnStop) <= target.lsn))
|
||||
{
|
||||
found = true;
|
||||
|
||||
result = restoreBackupData(backupData.backupLabel, repoIdx, infoPgCipherPass(infoBackupPg(infoBackup)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If a backup was found on this repo matching the criteria for time then exit
|
||||
if (found)
|
||||
break;
|
||||
}
|
||||
// Else use backup set found
|
||||
else
|
||||
{
|
||||
// Is this backup part of the latest pg history?
|
||||
const InfoPgData backupInfoPg = infoPgData(
|
||||
infoBackupPg(infoBackup), infoPgDataCurrentId(infoBackupPg(infoBackup)));
|
||||
|
||||
if (latestBackup.backupPgId < backupInfoPg.id)
|
||||
{
|
||||
THROW_FMT(
|
||||
BackupSetInvalidError,
|
||||
"the latest backup set found '%s' is from a prior version of " PG_NAME "\n"
|
||||
"HINT: was a backup created after the stanza-upgrade?\n"
|
||||
"HINT: specify --" CFGOPT_SET " or --" CFGOPT_TYPE "=time/lsn to restore from a prior version of"
|
||||
" " PG_NAME ".",
|
||||
strZ(latestBackup.backupLabel));
|
||||
}
|
||||
|
||||
result = restoreBackupData(latestBackup.backupLabel, repoIdx, infoPgCipherPass(infoBackupPg(infoBackup)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Otherwise check to see if the specified backup set is on this repo
|
||||
else
|
||||
{
|
||||
for (unsigned int backupIdx = 0; backupIdx < infoBackupDataTotal(infoBackup); backupIdx++)
|
||||
{
|
||||
if (strEq(infoBackupData(infoBackup, backupIdx).backupLabel, backupSetRequested))
|
||||
{
|
||||
result = restoreBackupData(backupSetRequested, repoIdx, infoPgCipherPass(infoBackupPg(infoBackup)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the backup set is found, then exit, else continue to next repo
|
||||
if (result.backupSet != NULL)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Still no backup set to use after checking all the repos required to be checked?
|
||||
if (result.backupSet == NULL)
|
||||
{
|
||||
if (backupSetRequested != NULL)
|
||||
THROW_FMT(BackupSetInvalidError, "backup set %s is not valid", strZ(backupSetRequested));
|
||||
else if (targetType == CFGOPTVAL_RESTORE_TYPE_TIME || targetType == CFGOPTVAL_RESTORE_TYPE_LSN)
|
||||
{
|
||||
THROW_FMT(
|
||||
BackupSetInvalidError, "unable to find backup set with %s '%s'",
|
||||
targetType == CFGOPTVAL_RESTORE_TYPE_LSN ? "lsn less than or equal to" : "stop time less than",
|
||||
strZ(cfgOptionDisplay(cfgOptTarget)));
|
||||
}
|
||||
else
|
||||
THROW(BackupSetInvalidError, "no backup set found to restore");
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
FUNCTION_LOG_RETURN_STRUCT(result);
|
||||
}
|
||||
|
||||
/***********************************************************************************************************************************
|
||||
Generate the expression to zero files that are not needed for selective restore
|
||||
***********************************************************************************************************************************/
|
||||
static String *
|
||||
restoreSelectiveExpression(const Manifest *const manifest)
|
||||
{
|
||||
FUNCTION_LOG_BEGIN(logLevelDebug);
|
||||
FUNCTION_LOG_PARAM(MANIFEST, manifest);
|
||||
FUNCTION_LOG_END();
|
||||
|
||||
ASSERT(manifest != NULL);
|
||||
|
||||
String *result = NULL;
|
||||
|
||||
// Continue if databases to include or exclude have been specified
|
||||
if (cfgOptionTest(cfgOptDbExclude) || cfgOptionTest(cfgOptDbInclude))
|
||||
{
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// Generate base expression
|
||||
RegExp *const baseRegExp = regExpNew(STRDEF("^" MANIFEST_TARGET_PGDATA "/" PG_PATH_BASE "/[0-9]+/" PG_FILE_PGVERSION));
|
||||
|
||||
// Generate tablespace expression
|
||||
const String *const tablespaceId = pgTablespaceId(
|
||||
manifestData(manifest)->pgVersion, manifestData(manifest)->pgCatalogVersion);
|
||||
RegExp *const tablespaceRegExp = regExpNew(
|
||||
strNewFmt("^" MANIFEST_TARGET_PGTBLSPC "/[0-9]+/%s/[0-9]+/" PG_FILE_PGVERSION, strZ(tablespaceId)));
|
||||
|
||||
// Generate a list of databases in base or in a tablespace and get all standard system databases, even in cases where
|
||||
// users have recreated them
|
||||
StringList *systemDbIdList = strLstNew();
|
||||
StringList *dbList = strLstNew();
|
||||
|
||||
for (unsigned int systemDbIdx = 0; systemDbIdx < manifestDbTotal(manifest); systemDbIdx++)
|
||||
{
|
||||
const ManifestDb *const systemDb = manifestDb(manifest, systemDbIdx);
|
||||
|
||||
if (pgDbIsSystem(systemDb->name) || pgDbIsSystemId(systemDb->id))
|
||||
{
|
||||
// Build the system id list and add to the dbList for logging and checking
|
||||
const String *const systemDbId = varStrForce(VARUINT(systemDb->id));
|
||||
|
||||
strLstAdd(systemDbIdList, systemDbId);
|
||||
strLstAdd(dbList, systemDbId);
|
||||
}
|
||||
}
|
||||
|
||||
for (unsigned int fileIdx = 0; fileIdx < manifestFileTotal(manifest); fileIdx++)
|
||||
{
|
||||
const String *const fileName = manifestFileNameGet(manifest, fileIdx);
|
||||
|
||||
if (regExpMatch(baseRegExp, fileName) || regExpMatch(tablespaceRegExp, fileName))
|
||||
{
|
||||
const String *const dbId = strBase(strPath(fileName));
|
||||
|
||||
// In the highly unlikely event that a system database was somehow added after the backup began, it will only be
|
||||
// found in the file list and not the manifest db section, so add it to the system database list
|
||||
if (pgDbIsSystemId(cvtZToUInt(strZ(dbId))))
|
||||
strLstAddIfMissing(systemDbIdList, dbId);
|
||||
|
||||
strLstAddIfMissing(dbList, dbId);
|
||||
}
|
||||
}
|
||||
|
||||
strLstSort(dbList, sortOrderAsc);
|
||||
|
||||
// If no databases were found then this backup is not a valid cluster
|
||||
if (strLstEmpty(dbList))
|
||||
THROW(FormatError, "no databases found for selective restore\nHINT: is this a valid cluster?");
|
||||
|
||||
// Log databases found
|
||||
LOG_DETAIL_FMT("databases found for selective restore (%s)", strZ(strLstJoin(dbList, ", ")));
|
||||
|
||||
// Generate list with ids of databases to exclude
|
||||
StringList *const excludeDbIdList = strLstNew();
|
||||
const StringList *const excludeList = strLstNewVarLst(cfgOptionLst(cfgOptDbExclude));
|
||||
|
||||
for (unsigned int excludeIdx = 0; excludeIdx < strLstSize(excludeList); excludeIdx++)
|
||||
{
|
||||
const String *excludeDb = strLstGet(excludeList, excludeIdx);
|
||||
|
||||
// If the db to exclude is not in the list as an id then search by name
|
||||
if (!strLstExists(dbList, excludeDb))
|
||||
{
|
||||
const ManifestDb *const db = manifestDbFindDefault(manifest, excludeDb, NULL);
|
||||
|
||||
if (db == NULL || !strLstExists(dbList, varStrForce(VARUINT(db->id))))
|
||||
THROW_FMT(DbMissingError, "database to exclude '%s' does not exist", strZ(excludeDb));
|
||||
|
||||
// Set the exclude db to the id if the name mapping was successful
|
||||
excludeDb = varStrForce(VARUINT(db->id));
|
||||
}
|
||||
|
||||
// Add to exclude list
|
||||
strLstAdd(excludeDbIdList, excludeDb);
|
||||
}
|
||||
|
||||
// Remove included databases from the list
|
||||
const StringList *const includeList = strLstNewVarLst(cfgOptionLst(cfgOptDbInclude));
|
||||
|
||||
for (unsigned int includeIdx = 0; includeIdx < strLstSize(includeList); includeIdx++)
|
||||
{
|
||||
const String *includeDb = strLstGet(includeList, includeIdx);
|
||||
|
||||
// If the db to include is not in the list as an id then search by name
|
||||
if (!strLstExists(dbList, includeDb))
|
||||
{
|
||||
const ManifestDb *const db = manifestDbFindDefault(manifest, includeDb, NULL);
|
||||
|
||||
if (db == NULL || !strLstExists(dbList, varStrForce(VARUINT(db->id))))
|
||||
THROW_FMT(DbMissingError, "database to include '%s' does not exist", strZ(includeDb));
|
||||
|
||||
// Set the include db to the id if the name mapping was successful
|
||||
includeDb = varStrForce(VARUINT(db->id));
|
||||
}
|
||||
|
||||
// Error if the db is a system db
|
||||
if (strLstExists(systemDbIdList, includeDb))
|
||||
THROW(DbInvalidError, "system databases (template0, postgres, etc.) are included by default");
|
||||
|
||||
// Error if the db id is in the exclude list
|
||||
if (strLstExists(excludeDbIdList, includeDb))
|
||||
THROW_FMT(DbInvalidError, "database to include '%s' is in the exclude list", strZ(includeDb));
|
||||
|
||||
// Remove from list of DBs to zero
|
||||
strLstRemove(dbList, includeDb);
|
||||
}
|
||||
|
||||
// Only exclude specified db in case no db to include has been provided
|
||||
if (strLstEmpty(includeList))
|
||||
{
|
||||
dbList = strLstDup(excludeDbIdList);
|
||||
}
|
||||
// Else, remove the system databases from list of DBs to zero unless they are excluded explicitly
|
||||
else
|
||||
{
|
||||
strLstSort(systemDbIdList, sortOrderAsc);
|
||||
strLstSort(excludeDbIdList, sortOrderAsc);
|
||||
systemDbIdList = strLstMergeAnti(systemDbIdList, excludeDbIdList);
|
||||
dbList = strLstMergeAnti(dbList, systemDbIdList);
|
||||
}
|
||||
|
||||
// Build regular expression to identify files that will be zeroed
|
||||
String *expression = NULL;
|
||||
|
||||
if (!strLstEmpty(dbList))
|
||||
{
|
||||
LOG_DETAIL_FMT("databases excluded (zeroed) from selective restore (%s)", strZ(strLstJoin(dbList, ", ")));
|
||||
|
||||
// Generate the expression from the list of databases to be zeroed. Only user created databases can be zeroed, never
|
||||
// system databases.
|
||||
for (unsigned int dbIdx = 0; dbIdx < strLstSize(dbList); dbIdx++)
|
||||
{
|
||||
const String *const db = strLstGet(dbList, dbIdx);
|
||||
|
||||
// Create expression string or append |
|
||||
if (expression == NULL)
|
||||
expression = strNew();
|
||||
else
|
||||
strCatZ(expression, "|");
|
||||
|
||||
// Filter files in base directory
|
||||
strCatFmt(expression, "(^" MANIFEST_TARGET_PGDATA "/" PG_PATH_BASE "/%s/)", strZ(db));
|
||||
|
||||
// Filter files in tablespace directories
|
||||
for (unsigned int targetIdx = 0; targetIdx < manifestTargetTotal(manifest); targetIdx++)
|
||||
{
|
||||
const ManifestTarget *const target = manifestTarget(manifest, targetIdx);
|
||||
|
||||
if (target->tablespaceId != 0)
|
||||
strCatFmt(expression, "|(^%s/%s/%s/)", strZ(target->name), strZ(tablespaceId), strZ(db));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all user databases have been selected then nothing to do
|
||||
if (expression == NULL)
|
||||
{
|
||||
LOG_INFO_FMT("nothing to filter - all user databases have been selected");
|
||||
}
|
||||
// Else return the expression
|
||||
else
|
||||
{
|
||||
MEM_CONTEXT_PRIOR_BEGIN()
|
||||
{
|
||||
result = strDup(expression);
|
||||
}
|
||||
MEM_CONTEXT_PRIOR_END();
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
}
|
||||
|
||||
FUNCTION_LOG_RETURN(STRING, result);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/***********************************************************************************************************************************
|
||||
Validate restore path
|
||||
***********************************************************************************************************************************/
|
||||
static void
|
||||
restorePathValidate(void)
|
||||
{
|
||||
FUNCTION_LOG_VOID(logLevelDebug);
|
||||
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// PostgreSQL must not be running
|
||||
if (storageExistsP(storagePg(), PG_FILE_POSTMTRPID_STR))
|
||||
{
|
||||
THROW_FMT(
|
||||
PgRunningError,
|
||||
"unable to restore while PostgreSQL is running\n"
|
||||
"HINT: presence of '" PG_FILE_POSTMTRPID "' in '%s' indicates PostgreSQL is running.\n"
|
||||
"HINT: remove '" PG_FILE_POSTMTRPID "' only if PostgreSQL is not running.",
|
||||
strZ(cfgOptionDisplay(cfgOptPgPath)));
|
||||
}
|
||||
|
||||
// If the restore will be destructive attempt to verify that PGDATA looks like a valid PostgreSQL directory
|
||||
if ((cfgOptionBool(cfgOptDelta) || cfgOptionBool(cfgOptForce)) &&
|
||||
!storageExistsP(storagePg(), PG_FILE_PGVERSION_STR) && !storageExistsP(storagePg(), BACKUP_MANIFEST_FILE_STR))
|
||||
{
|
||||
LOG_WARN_FMT(
|
||||
"--delta or --force specified but unable to find '" PG_FILE_PGVERSION "' or '" BACKUP_MANIFEST_FILE "' in '%s' to"
|
||||
" confirm that this is a valid $PGDATA directory. --delta and --force have been disabled and if any files exist"
|
||||
" in the destination directories the restore will be aborted.",
|
||||
strZ(cfgOptionDisplay(cfgOptPgPath)));
|
||||
|
||||
// Disable delta and force so restore will fail if the directories are not empty
|
||||
cfgOptionSet(cfgOptDelta, cfgSourceDefault, BOOL_FALSE_VAR);
|
||||
cfgOptionSet(cfgOptForce, cfgSourceDefault, BOOL_FALSE_VAR);
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
FUNCTION_LOG_RETURN_VOID();
|
||||
}
|
||||
|
||||
/***********************************************************************************************************************************
|
||||
Validate the manifest
|
||||
***********************************************************************************************************************************/
|
||||
static void
|
||||
restoreManifestValidate(const Manifest *const manifest, const String *const backupSet)
|
||||
{
|
||||
FUNCTION_LOG_BEGIN(logLevelDebug);
|
||||
FUNCTION_LOG_PARAM(MANIFEST, manifest);
|
||||
FUNCTION_LOG_PARAM(STRING, backupSet);
|
||||
FUNCTION_LOG_END();
|
||||
|
||||
ASSERT(manifest != NULL);
|
||||
ASSERT(backupSet != NULL);
|
||||
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// If there are no files in the manifest then something has gone horribly wrong
|
||||
CHECK(FormatError, manifestFileTotal(manifest) > 0, "manifest missing files");
|
||||
|
||||
// Sanity check to ensure the manifest has not been moved to a new directory
|
||||
const ManifestData *const data = manifestData(manifest);
|
||||
|
||||
if (!strEq(data->backupLabel, backupSet))
|
||||
{
|
||||
THROW_FMT(
|
||||
FormatError,
|
||||
"requested backup '%s' and manifest label '%s' do not match\n"
|
||||
"HINT: this indicates some sort of corruption (at the very least paths have been renamed).",
|
||||
strZ(backupSet), strZ(data->backupLabel));
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
|
||||
FUNCTION_LOG_RETURN_VOID();
|
||||
}
|
||||
@@ -960,10 +960,16 @@ unit:
|
||||
coverage:
|
||||
- command/restore/blockChecksum
|
||||
- command/restore/blockDelta
|
||||
- command/restore/clean.inc: included
|
||||
- command/restore/config.inc: included
|
||||
- command/restore/file
|
||||
- command/restore/process.inc: included
|
||||
- command/restore/protocol
|
||||
- command/restore/remap.inc: included
|
||||
- command/restore/restore
|
||||
- command/restore/select.inc: included
|
||||
- command/restore/timeline
|
||||
- command/restore/validate.inc: included
|
||||
|
||||
include:
|
||||
- common/user
|
||||
|
||||
@@ -985,6 +985,8 @@ testCvgGenerate(
|
||||
|
||||
if (strEndsWithZ(coverageName, ".vendor.c"))
|
||||
coverageName = strNewFmt("%s.inc", strZ(coverageName));
|
||||
else if (strEndsWithZ(coverageName, ".inc.c"))
|
||||
coverageName = strNewFmt("%s.c.inc", strZ(strSubN(coverageName, 0, strSize(coverageName) - 6)));
|
||||
|
||||
strLstAdd(coverageModList, coverageName);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ testRun(void)
|
||||
" - name: error\n"
|
||||
" total: 1\n"
|
||||
" coverage:\n"
|
||||
" - common/error/sub/error.inc: included\n"
|
||||
" - common/error/sub/error.vendor: included\n"
|
||||
" - doc/common/error/error: included\n"
|
||||
" - test/common/error/error\n"
|
||||
@@ -75,6 +76,14 @@ testRun(void)
|
||||
/* 04 */ " return code;\n"
|
||||
/* 05 */ "}\n");
|
||||
|
||||
HRN_STORAGE_PUT_Z(
|
||||
storageTest, "test/repo/src/common/error/sub/error.c.inc",
|
||||
/* 01 */ "int\n"
|
||||
/* 02 */ "returnCodeInc(int code)\n"
|
||||
/* 03 */ "{\n"
|
||||
/* 04 */ " return code;\n"
|
||||
/* 05 */ "}\n");
|
||||
|
||||
HRN_STORAGE_PUT_Z(
|
||||
storageTest, "test/repo/src/common/error/sub/error.vendor.c.inc",
|
||||
/* 01 */ "int\n"
|
||||
@@ -137,6 +146,23 @@ testRun(void)
|
||||
"\"end_line\": 5"
|
||||
"}"
|
||||
"],"
|
||||
"\"file\": \"../../../repo/test/src/common/error/sub/error.c.inc\""
|
||||
"},"
|
||||
"{"
|
||||
"\"lines\": ["
|
||||
"{"
|
||||
"\"count\": 1,"
|
||||
"\"line_number\": 4"
|
||||
"}"
|
||||
"],"
|
||||
"\"functions\": ["
|
||||
"{"
|
||||
"\"start_line\": 2,"
|
||||
"\"name\": \"returnCodeInc\","
|
||||
"\"execution_count\": 1,"
|
||||
"\"end_line\": 5"
|
||||
"}"
|
||||
"],"
|
||||
"\"file\": \"../../../repo/test/src/common/error/sub/error.vendor.c.inc\""
|
||||
"},"
|
||||
"{"
|
||||
@@ -405,6 +431,7 @@ testRun(void)
|
||||
TEST_CVG_HTML_PRE
|
||||
TEST_CVG_HTML_TOC_PRE
|
||||
TEST_CVG_HTML_TOC_COVERED_PRE "doc/src/common/error/error.c" TEST_CVG_HTML_TOC_COVERED_POST
|
||||
TEST_CVG_HTML_TOC_COVERED_PRE "src/common/error/sub/error.c.inc" TEST_CVG_HTML_TOC_COVERED_POST
|
||||
TEST_CVG_HTML_TOC_COVERED_PRE "src/common/error/sub/error.vendor.c.inc" TEST_CVG_HTML_TOC_COVERED_POST
|
||||
TEST_CVG_HTML_TOC_POST
|
||||
TEST_CVG_HTML_POST);
|
||||
@@ -432,16 +459,16 @@ testRun(void)
|
||||
"\n"
|
||||
"<table-row>\n"
|
||||
" <table-cell>common/error/sub</table-cell>\n"
|
||||
" <table-cell>1/1 (100.00%)</table-cell>\n"
|
||||
" <table-cell>2/2 (100.00%)</table-cell>\n"
|
||||
" <table-cell>---</table-cell>\n"
|
||||
" <table-cell>1/1 (100.00%)</table-cell>\n"
|
||||
" <table-cell>2/2 (100.00%)</table-cell>\n"
|
||||
"</table-row>\n"
|
||||
"\n"
|
||||
"<table-row>\n"
|
||||
" <table-cell>TOTAL</table-cell>\n"
|
||||
" <table-cell>3/4 (75.00%)</table-cell>\n"
|
||||
" <table-cell>4/5 (80.00%)</table-cell>\n"
|
||||
" <table-cell>2/4 (50.00%)</table-cell>\n"
|
||||
" <table-cell>7/12 (58.33%)</table-cell>\n"
|
||||
" <table-cell>8/13 (61.54%)</table-cell>\n"
|
||||
"</table-row>\n");
|
||||
|
||||
// -------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -458,6 +485,7 @@ testRun(void)
|
||||
|
||||
TEST_CVG_HTML_TOC_PRE
|
||||
TEST_CVG_HTML_TOC_COVERED_PRE "doc/src/common/error/error.c" TEST_CVG_HTML_TOC_COVERED_POST
|
||||
TEST_CVG_HTML_TOC_COVERED_PRE "src/common/error/sub/error.c.inc" TEST_CVG_HTML_TOC_COVERED_POST
|
||||
TEST_CVG_HTML_TOC_COVERED_PRE "src/common/error/sub/error.vendor.c.inc" TEST_CVG_HTML_TOC_COVERED_POST
|
||||
TEST_CVG_HTML_TOC_UNCOVERED_PRE "src/common/log.c" TEST_CVG_HTML_TOC_UNCOVERED_MID "src/common/log.c"
|
||||
TEST_CVG_HTML_TOC_UNCOVERED_POST
|
||||
|
||||
Reference in New Issue
Block a user