diff --git a/doc/xml/release/2020s/2026/2.59.0.xml b/doc/xml/release/2020s/2026/2.59.0.xml
index 9c061420d..0b91e1179 100644
--- a/doc/xml/release/2020s/2026/2.59.0.xml
+++ b/doc/xml/release/2020s/2026/2.59.0.xml
@@ -12,5 +12,19 @@
Suppress unused parameter errors in meson compiler probes.
+
+
+
+
+
+
+
+
+
+
+
+ Refactor restore module into included modules.
+
+
diff --git a/src/command/restore/clean.c.inc b/src/command/restore/clean.c.inc
new file mode 100644
index 000000000..aa838284f
--- /dev/null
+++ b/src/command/restore/clean.c.inc
@@ -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();
+}
diff --git a/src/command/restore/config.c.inc b/src/command/restore/config.c.inc
new file mode 100644
index 000000000..70e9d8c62
--- /dev/null
+++ b/src/command/restore/config.c.inc
@@ -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();
+}
diff --git a/src/command/restore/process.c.inc b/src/command/restore/process.c.inc
new file mode 100644
index 000000000..b6e37368f
--- /dev/null
+++ b/src/command/restore/process.c.inc
@@ -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);
+}
diff --git a/src/command/restore/remap.c.inc b/src/command/restore/remap.c.inc
new file mode 100644
index 000000000..19096ed85
--- /dev/null
+++ b/src/command/restore/remap.c.inc
@@ -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();
+}
diff --git a/src/command/restore/restore.c b/src/command/restore/restore.c
index d3f4a014d..676df5c16 100644
--- a/src/command/restore/restore.c
+++ b/src/command/restore/restore.c
@@ -3,9 +3,6 @@ Restore Command
***********************************************************************************************************************************/
#include "build.auto.h"
-#include
-#include
-#include
#include
#include "command/lock.h"
@@ -13,12 +10,8 @@ Restore Command
#include "command/restore/protocol.h"
#include "command/restore/restore.h"
#include "command/restore/timeline.h"
-#include "common/crypto/cipherBlock.h"
-#include "common/debug.h"
-#include "common/log.h"
#include "common/regExp.h"
#include "common/user.h"
-#include "config/config.h"
#include "config/exec.h"
#include "info/infoArchive.h"
#include "info/infoBackup.h"
@@ -28,2329 +21,13 @@ Restore Command
#include "protocol/helper.h"
#include "protocol/parallel.h"
#include "storage/helper.h"
-#include "storage/write.h"
-#include "version.h"
-/***********************************************************************************************************************************
-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"
-
-/***********************************************************************************************************************************
-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();
-}
-
-/***********************************************************************************************************************************
-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);
-}
-
-/***********************************************************************************************************************************
-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();
-}
-
-/***********************************************************************************************************************************
-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();
-}
-
-/***********************************************************************************************************************************
-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();
-}
-
-/***********************************************************************************************************************************
-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);
-}
-
-/***********************************************************************************************************************************
-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();
-}
-
-/***********************************************************************************************************************************
-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);
-}
+#include "clean.c.inc"
+#include "config.c.inc"
+#include "process.c.inc"
+#include "remap.c.inc"
+#include "select.c.inc"
+#include "validate.c.inc"
/**********************************************************************************************************************************/
FN_EXTERN void
diff --git a/src/command/restore/select.c.inc b/src/command/restore/select.c.inc
new file mode 100644
index 000000000..5dba02f90
--- /dev/null
+++ b/src/command/restore/select.c.inc
@@ -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);
+}
diff --git a/src/command/restore/validate.c.inc b/src/command/restore/validate.c.inc
new file mode 100644
index 000000000..5a1d4c898
--- /dev/null
+++ b/src/command/restore/validate.c.inc
@@ -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();
+}
diff --git a/test/define.yaml b/test/define.yaml
index 6dcbed3bf..b184f7891 100644
--- a/test/define.yaml
+++ b/test/define.yaml
@@ -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
diff --git a/test/src/command/test/coverage.c b/test/src/command/test/coverage.c
index 5aece66fc..272371b44 100644
--- a/test/src/command/test/coverage.c
+++ b/test/src/command/test/coverage.c
@@ -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);
}
diff --git a/test/src/module/test/coverageTest.c b/test/src/module/test/coverageTest.c
index 4a07f371f..8f2958643 100644
--- a/test/src/module/test/coverageTest.c
+++ b/test/src/module/test/coverageTest.c
@@ -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"
"\n"
" common/error/sub\n"
- " 1/1 (100.00%)\n"
+ " 2/2 (100.00%)\n"
" ---\n"
- " 1/1 (100.00%)\n"
+ " 2/2 (100.00%)\n"
"\n"
"\n"
"\n"
" TOTAL\n"
- " 3/4 (75.00%)\n"
+ " 4/5 (80.00%)\n"
" 2/4 (50.00%)\n"
- " 7/12 (58.33%)\n"
+ " 8/13 (61.54%)\n"
"\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