1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2025-02-21 19:48:29 +02:00

Move link creation to storage interface.

Direct link creation via Posix functions has been moved to the Posix driver.

This change allows adding SFTP softlink creation in the SFTP driver using the standard interface.
This commit is contained in:
Reid Thompson 2022-10-01 21:26:44 -04:00 committed by GitHub
parent 2a4137ed2e
commit 01b81f9d37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 375 additions and 19 deletions

View File

@ -32,6 +32,20 @@
<p>Fix memory leak in file bundle <cmd>backup</cmd>/<cmd>restore</cmd>.</p>
</release-item>
</release-bug-list>
<release-development-list>
<release-item>
<github-pull-request id="1879"/>
<release-item-contributor-list>
<release-item-contributor id="reid.thompson"/>
<release-item-reviewer id="david.steele"/>
<release-item-reviewer id="john.morris"/>
</release-item-contributor-list>
<p>Move link creation to storage interface.</p>
</release-item>
</release-development-list>
</release-core-list>
</release>

View File

@ -1842,9 +1842,7 @@ backupProcess(
const String *const linkDestination = strNewFmt(
"../../" MANIFEST_TARGET_PGTBLSPC "/%u", target->tablespaceId);
THROW_ON_SYS_ERROR_FMT(
symlink(strZ(linkDestination), strZ(link)) == -1, FileOpenError,
"unable to create symlink '%s' to '%s'", strZ(link), strZ(linkDestination));
storageLinkCreateP(storageRepoWrite(), linkDestination, link);
}
}
}
@ -1955,9 +1953,7 @@ backupProcess(
storageRepo(),
strNewFmt(STORAGE_REPO_BACKUP "/%s/%s%s", strZ(file.reference), strZ(file.name), compressExt));
THROW_ON_SYS_ERROR_FMT(
link(strZ(linkDestination), strZ(linkName)) == -1, FileOpenError,
"unable to create hardlink '%s' to '%s'", strZ(linkName), strZ(linkDestination));
storageLinkCreateP(storageRepoWrite(), linkDestination, linkName, .linkType = storageLinkHard);
}
// Else log the reference. With delta, it is possible that references may have been removed if a file needed to be
// recopied.

View File

@ -137,11 +137,7 @@ backupLinkLatest(const String *backupLabel, unsigned int repoIdx)
storageRemoveP(storageRepoIdxWrite(repoIdx), latestLink);
if (storageFeature(storageRepoIdxWrite(repoIdx), storageFeatureSymLink))
{
THROW_ON_SYS_ERROR_FMT(
symlink(strZ(backupLabel), strZ(latestLink)) == -1, FileOpenError, "unable to create symlink '%s' to '%s'",
strZ(latestLink), strZ(backupLabel));
}
storageLinkCreateP(storageRepoIdxWrite(repoIdx), backupLabel, latestLink);
// Sync backup path if required
if (storageFeature(storageRepoIdxWrite(repoIdx), storageFeaturePathSync))

View File

@ -1265,9 +1265,7 @@ restoreCleanBuild(const Manifest *const manifest, const String *const rootReplac
{
LOG_DETAIL_FMT("create symlink '%s' to '%s'", strZ(pgPath), strZ(link->destination));
THROW_ON_SYS_ERROR_FMT(
symlink(strZ(link->destination), strZ(pgPath)) == -1, FileOpenError,
"unable to 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);
}
@ -1305,9 +1303,7 @@ restoreCleanBuild(const Manifest *const manifest, const String *const rootReplac
{
LOG_DETAIL_FMT("create symlink '%s' to '%s'", strZ(pgPath), strZ(link->destination));
THROW_ON_SYS_ERROR_FMT(
symlink(strZ(link->destination), strZ(pgPath)) == -1, FileOpenError,
"unable to 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);
}

View File

@ -112,6 +112,44 @@ storagePosixInfo(THIS_VOID, const String *file, StorageInfoLevel level, StorageI
FUNCTION_LOG_RETURN(STORAGE_INFO, result);
}
/**********************************************************************************************************************************/
void
storagePosixLinkCreate(
THIS_VOID, const String *const target, const String *const linkPath, const StorageInterfaceLinkCreateParam param)
{
THIS(StoragePosix);
FUNCTION_LOG_BEGIN(logLevelTrace);
FUNCTION_LOG_PARAM(STORAGE_POSIX, this);
FUNCTION_LOG_PARAM(STRING, target);
FUNCTION_LOG_PARAM(STRING, linkPath);
FUNCTION_LOG_PARAM(ENUM, param.linkType);
FUNCTION_LOG_END();
ASSERT(this != NULL);
ASSERT(target != NULL);
ASSERT(linkPath != NULL);
// Create symlink
if (param.linkType == storageLinkSym)
{
THROW_ON_SYS_ERROR_FMT(
symlink(strZ(target), strZ(linkPath)) == -1, FileOpenError, "unable to create symlink '%s' to '%s'", strZ(linkPath),
strZ(target));
}
// Else create hardlink
else
{
ASSERT(param.linkType == storageLinkHard);
THROW_ON_SYS_ERROR_FMT(
link(strZ(target), strZ(linkPath)) == -1, FileOpenError, "unable to create hardlink '%s' to '%s'", strZ(linkPath),
strZ(target));
}
FUNCTION_LOG_RETURN_VOID();
}
/**********************************************************************************************************************************/
// Helper function to get info for a file if it exists. This logic can't live directly in storagePosixList() because there is a race
// condition where a file might exist while listing the directory but it is gone before stat() can be called. In order to get
@ -534,6 +572,7 @@ static const StorageInterface storageInterfacePosix =
.feature = 1 << storageFeaturePath,
.info = storagePosixInfo,
.linkCreate = storagePosixLinkCreate,
.list = storagePosixList,
.move = storagePosixMove,
.newRead = storagePosixNewRead,

View File

@ -17,6 +17,7 @@ Storage *storagePosixNewInternal(
/***********************************************************************************************************************************
Functions
***********************************************************************************************************************************/
void storagePosixLinkCreate(THIS_VOID, const String *target, const String *linkPath, StorageInterfaceLinkCreateParam param);
void storagePosixPathCreate(
THIS_VOID, const String *path, bool errorOnExists, bool noParentCreate, mode_t mode, StorageInterfacePathCreateParam param);
void storagePosixPathSync(THIS_VOID, const String *path, StorageInterfacePathSyncParam param);

View File

@ -279,6 +279,33 @@ storageRemoteInfoProtocol(PackRead *const param, ProtocolServer *const server)
FUNCTION_LOG_RETURN_VOID();
}
/**********************************************************************************************************************************/
void
storageRemoteLinkCreateProtocol(PackRead *const param, ProtocolServer *const server)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(PACK_READ, param);
FUNCTION_LOG_PARAM(PROTOCOL_SERVER, server);
FUNCTION_LOG_END();
ASSERT(param != NULL);
ASSERT(server != NULL);
ASSERT(storageRemoteProtocolLocal.driver != NULL);
MEM_CONTEXT_TEMP_BEGIN()
{
const String *const target = pckReadStrP(param);
const String *const linkPath = pckReadStrP(param);
const StorageLinkType linkType = (StorageLinkType)pckReadU32P(param);
storageInterfaceLinkCreateP(storageRemoteProtocolLocal.driver, target, linkPath, .linkType = linkType);
protocolServerDataEndPut(server);
}
MEM_CONTEXT_TEMP_END();
FUNCTION_LOG_RETURN_VOID();
}
/**********************************************************************************************************************************/
void
storageRemoteListProtocol(PackRead *const param, ProtocolServer *const server)

View File

@ -13,6 +13,7 @@ Functions
// Process storage protocol requests
void storageRemoteFeatureProtocol(PackRead *param, ProtocolServer *server);
void storageRemoteInfoProtocol(PackRead *param, ProtocolServer *server);
void storageRemoteLinkCreateProtocol(PackRead *param, ProtocolServer *server);
void storageRemoteListProtocol(PackRead *param, ProtocolServer *server);
void storageRemoteOpenReadProtocol(PackRead *param, ProtocolServer *server);
void storageRemoteOpenWriteProtocol(PackRead *param, ProtocolServer *server);
@ -26,6 +27,7 @@ Protocol commands for ProtocolServerHandler arrays passed to protocolServerProce
***********************************************************************************************************************************/
#define PROTOCOL_COMMAND_STORAGE_FEATURE STRID5("s-f", 0x1b730)
#define PROTOCOL_COMMAND_STORAGE_INFO STRID5("s-i", 0x27730)
#define PROTOCOL_COMMAND_STORAGE_LINK_CREATE STRID5("s-lc", 0x1b3730)
#define PROTOCOL_COMMAND_STORAGE_LIST STRID5("s-l", 0x33730)
#define PROTOCOL_COMMAND_STORAGE_OPEN_READ STRID5("s-or", 0x93f730)
#define PROTOCOL_COMMAND_STORAGE_OPEN_WRITE STRID5("s-ow", 0xbbf730)
@ -37,6 +39,7 @@ Protocol commands for ProtocolServerHandler arrays passed to protocolServerProce
#define PROTOCOL_SERVER_HANDLER_STORAGE_REMOTE_LIST \
{.command = PROTOCOL_COMMAND_STORAGE_FEATURE, .handler = storageRemoteFeatureProtocol}, \
{.command = PROTOCOL_COMMAND_STORAGE_INFO, .handler = storageRemoteInfoProtocol}, \
{.command = PROTOCOL_COMMAND_STORAGE_LINK_CREATE, .handler = storageRemoteLinkCreateProtocol}, \
{.command = PROTOCOL_COMMAND_STORAGE_LIST, .handler = storageRemoteListProtocol}, \
{.command = PROTOCOL_COMMAND_STORAGE_OPEN_READ, .handler = storageRemoteOpenReadProtocol}, \
{.command = PROTOCOL_COMMAND_STORAGE_OPEN_WRITE, .handler = storageRemoteOpenWriteProtocol}, \

View File

@ -167,6 +167,40 @@ storageRemoteInfo(THIS_VOID, const String *file, StorageInfoLevel level, Storage
FUNCTION_LOG_RETURN(STORAGE_INFO, result);
}
/**********************************************************************************************************************************/
static void
storageRemoteLinkCreate(
THIS_VOID, const String *const target, const String *const linkPath, const StorageInterfaceLinkCreateParam param)
{
THIS(StorageRemote);
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(STORAGE_REMOTE, this);
FUNCTION_LOG_PARAM(STRING, target);
FUNCTION_LOG_PARAM(STRING, linkPath);
FUNCTION_LOG_PARAM(ENUM, param.linkType);
FUNCTION_LOG_END();
ASSERT(this != NULL);
ASSERT(target != NULL);
ASSERT(linkPath != NULL);
MEM_CONTEXT_TEMP_BEGIN()
{
ProtocolCommand *const command = protocolCommandNew(PROTOCOL_COMMAND_STORAGE_LINK_CREATE);
PackWrite *const commandParam = protocolCommandParam(command);
pckWriteStrP(commandParam, target);
pckWriteStrP(commandParam, linkPath);
pckWriteU32P(commandParam, param.linkType);
protocolClientExecute(this->client, command, false);
}
MEM_CONTEXT_TEMP_END();
FUNCTION_LOG_RETURN_VOID();
}
/**********************************************************************************************************************************/
static StorageList *
storageRemoteList(THIS_VOID, const String *const path, const StorageInfoLevel level, const StorageInterfaceListParam param)
@ -427,6 +461,7 @@ static const StorageInterface storageInterfaceRemote =
.pathRemove = storageRemotePathRemove,
.pathSync = storageRemotePathSync,
.remove = storageRemoteRemove,
.linkCreate = storageRemoteLinkCreate,
};
Storage *

View File

@ -89,6 +89,13 @@ storageNew(
AssertError, !storageFeature(this, storageFeatureSymLink) || storageFeature(this, storageFeaturePath),
"path feature required");
// If link features are enabled then linkCreate must be implemented
CHECK(
AssertError,
(!storageFeature(this, storageFeatureSymLink) && !storageFeature(this, storageFeatureHardLink)) ||
interface.linkCreate != NULL,
"linkCreate required");
FUNCTION_LOG_RETURN(STORAGE, this);
}
@ -317,6 +324,35 @@ storageNewItr(const Storage *const this, const String *const pathExp, StorageNew
FUNCTION_LOG_RETURN(STORAGE_ITERATOR, result);
}
/**********************************************************************************************************************************/
void storageLinkCreate(
const Storage *const this, const String *const target, const String *const linkPath, const StorageLinkCreateParam param)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(STORAGE, this);
FUNCTION_LOG_PARAM(STRING, target);
FUNCTION_LOG_PARAM(STRING, linkPath);
FUNCTION_LOG_PARAM(ENUM, param.linkType);
FUNCTION_LOG_END();
ASSERT(this != NULL);
ASSERT(this->write);
ASSERT(target != NULL);
ASSERT(linkPath != NULL);
ASSERT(this->pub.interface.linkCreate != NULL);
ASSERT(
(param.linkType == storageLinkSym && storageFeature(this, storageFeatureSymLink)) ||
(param.linkType == storageLinkHard && storageFeature(this, storageFeatureHardLink)));
MEM_CONTEXT_TEMP_BEGIN()
{
storageInterfaceLinkCreateP(storageDriver(this), target, linkPath, .linkType = param.linkType);
}
MEM_CONTEXT_TEMP_END();
FUNCTION_LOG_RETURN_VOID();
}
/**********************************************************************************************************************************/
StringList *
storageList(const Storage *this, const String *pathExp, StorageListParam param)

View File

@ -6,6 +6,18 @@ Storage Interface
#include <sys/types.h>
/***********************************************************************************************************************************
Storage link type
***********************************************************************************************************************************/
typedef enum
{
// Symbolic (or soft) link
storageLinkSym,
// Hard link
storageLinkHard,
} StorageLinkType;
/***********************************************************************************************************************************
Object type
***********************************************************************************************************************************/
@ -241,6 +253,20 @@ typedef struct StorageRemoveParam
void storageRemove(const Storage *this, const String *fileExp, StorageRemoveParam param);
// Create a hard or symbolic link
typedef struct StorageLinkCreateParam
{
VAR_PARAM_HEADER;
// Flag to create hard or symbolic link
StorageLinkType linkType;
} StorageLinkCreateParam;
#define storageLinkCreateP(this, target, linkPath, ...) \
storageLinkCreate(this, target, linkPath, (StorageLinkCreateParam){VAR_PARAM_INIT, __VA_ARGS__})
void storageLinkCreate(const Storage *this, const String *target, const String *linkPath, StorageLinkCreateParam param);
/***********************************************************************************************************************************
Getters/Setters
***********************************************************************************************************************************/

View File

@ -86,6 +86,23 @@ typedef StorageInfo StorageInterfaceInfo(
#define storageInterfaceInfoP(thisVoid, file, level, ...) \
STORAGE_COMMON_INTERFACE(thisVoid).info(thisVoid, file, level, (StorageInterfaceInfoParam){VAR_PARAM_INIT, __VA_ARGS__})
// ---------------------------------------------------------------------------------------------------------------------------------
// Create a hard or symbolic link
typedef struct StorageInterfaceLinkCreateParam
{
VAR_PARAM_HEADER;
// Flag to create hard or symbolic link
StorageLinkType linkType;
} StorageInterfaceLinkCreateParam;
typedef void StorageInterfaceLinkCreate(
void *thisVoid, const String *target, const String *linkPath, StorageInterfaceLinkCreateParam param);
#define storageInterfaceLinkCreateP(thisVoid, target, linkPath, ...) \
STORAGE_COMMON_INTERFACE(thisVoid).linkCreate(thisVoid, target, linkPath, \
(StorageInterfaceLinkCreateParam){VAR_PARAM_INIT, __VA_ARGS__})
// ---------------------------------------------------------------------------------------------------------------------------------
// Create a file read object. The file should not be opened immediately -- open() will be called on the IoRead interface when the
// file needs to be opened.
@ -275,6 +292,7 @@ typedef struct StorageInterface
StorageInterfaceRemove *remove;
// Optional functions
StorageInterfaceLinkCreate *linkCreate;
StorageInterfaceMove *move;
StorageInterfacePathCreate *pathCreate;
StorageInterfacePathSync *pathSync;

View File

@ -483,7 +483,7 @@ unit:
test:
# ----------------------------------------------------------------------------------------------------------------------------
- name: posix
total: 22
total: 23
coverage:
- storage/cifs/helper
@ -500,7 +500,7 @@ unit:
# ----------------------------------------------------------------------------------------------------------------------------
- name: remote
total: 9
total: 10
coverage:
- storage/remote/read

View File

@ -860,6 +860,50 @@ testRun(void)
TEST_RESULT_VOID(storagePathSyncP(storageTest, pathName), "sync path");
}
// *****************************************************************************************************************************
if (testBegin("storageLinkCreate()"))
{
StorageInfo info = {0};
const String *backupLabel = STRDEF("20181119-152138F");
const String *latestLabel = STRDEF("latest");
int invalidLinkType = 9;
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("create soft link to BACKUPLABEL");
TEST_RESULT_VOID(storagePathCreateP(storageTest, backupLabel), "create path to link to");
TEST_RESULT_VOID(storageLinkCreateP(storageTest, backupLabel, latestLabel), "create symlink");
TEST_ASSIGN(info, storageInfoP(storageTest, latestLabel, .ignoreMissing = false), "get link info");
TEST_RESULT_STR(info.linkDestination, backupLabel, "match link destination");
TEST_RESULT_VOID(storageRemoveP(storageTest, latestLabel), "remove symlink");
TEST_RESULT_VOID(storageLinkCreateP(storageTest, backupLabel, latestLabel, .linkType = storageLinkSym), "create symlink");
TEST_ASSIGN(info, storageInfoP(storageTest, latestLabel, .ignoreMissing = false), "get link info");
TEST_RESULT_STR(info.linkDestination, backupLabel, "match link destination");
TEST_RESULT_VOID(storageRemoveP(storageTest, latestLabel), "remove symlink");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("hardlink success/fail");
const String *emptyFile = STRDEF(TEST_PATH "/test.empty");
TEST_RESULT_VOID(storagePutP(storageNewWriteP(storageTest, emptyFile), NULL), "put empty file");
TEST_RESULT_VOID(
storageLinkCreateP(storageTest, emptyFile, latestLabel, .linkType = storageLinkHard), "hardlink to empty file");
TEST_RESULT_VOID(storageRemoveP(storageTest, emptyFile), "remove empty file");
TEST_RESULT_VOID(storageRemoveP(storageTest, latestLabel), "remove latest label");
TEST_ERROR(
storageLinkCreateP(storageTest, backupLabel, latestLabel, .linkType = storageLinkHard), FileOpenError,
"unable to create hardlink \'latest\' to \'20181119-152138F\': [1] Operation not permitted");
TEST_ASSIGN(info, storageInfoP(storageTest, latestLabel, .ignoreMissing = true), "get link info");
TEST_RESULT_STR(info.linkDestination, NULL, "no link destination");
TEST_RESULT_VOID(storagePathRemoveP(storageTest, backupLabel), "remove backup path");
TEST_ERROR(
storageLinkCreateP(storageTest, backupLabel, latestLabel, .linkType = invalidLinkType), AssertError,
"assertion '(param.linkType == storageLinkSym && storageFeature(this, storageFeatureSymLink)) ||"
" (param.linkType == storageLinkHard && storageFeature(this, storageFeatureHardLink))' failed");
}
// *****************************************************************************************************************************
if (testBegin("storageNewRead()"))
{

View File

@ -524,6 +524,131 @@ testRun(void)
TEST_RESULT_VOID(storagePathSyncP(storageRepoWrite, path), "sync path");
}
// *****************************************************************************************************************************
if (testBegin("storageLinkCreate()"))
{
StorageInfo info = {0};
const String *path = STRDEF("20181119-152138F");
const String *latestLabel = STRDEF("latest");
const String *testFile = strNewFmt("%s/%s", strZ(storagePathP(storageRepo, NULL)), "test.file");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("create/remove symlink to path and to file");
// Create the path and symlink via the remote
TEST_RESULT_VOID(storagePathCreateP(storageRepoWrite, path), "remote create path to link to");
TEST_RESULT_VOID(
storageLinkCreateP(
storageRepoWrite, path, strNewFmt("%s/%s", strZ(storagePathP(storageRepo, NULL)), strZ(latestLabel))),
"remote create path symlink");
// Check the repo via the local test storage to ensure the remote wrote to it, then remove via remote and confirm removed
TEST_RESULT_BOOL(
storageInfoP(storageTest, strNewFmt("repo128/%s", strZ(path)), .ignoreMissing = true).exists, true, "path exists");
TEST_RESULT_BOOL(
storageInfoP(
storageTest, strNewFmt("repo128/%s", strZ(latestLabel)), .ignoreMissing = true).exists, true,
"symlink to path exists");
// Verify link mapping via the local test storage
TEST_ASSIGN(
info,
storageInfoP(storageTest, strNewFmt("repo128/%s", strZ(latestLabel)), .ignoreMissing = false), "get symlink info");
TEST_RESULT_STR(info.linkDestination, path, "match link destination");
TEST_RESULT_INT(info.type, storageTypeLink, "check type is link");
TEST_RESULT_VOID(storageRemoveP(storageRepoWrite, latestLabel), "remote remove symlink");
TEST_RESULT_BOOL(
storageInfoP(
storageTest, strNewFmt("repo128/%s", strZ(latestLabel)), .ignoreMissing = true).exists, false,
"symlink to path removed");
// Create a file and sym link to it via the remote,
TEST_RESULT_VOID(storagePutP(storageNewWriteP(storageRepoWrite, testFile), BUFSTRDEF("TESTME")), "put test file");
TEST_RESULT_VOID(
storageLinkCreateP(
storageRepoWrite, testFile, strNewFmt("%s/%s", strZ(storagePathP(storageRepo, NULL)), strZ(latestLabel))),
"remote create file symlink");
// Check the repo via the local test storage to ensure the remote wrote to it, then remove via remote and confirm removed
TEST_RESULT_BOOL(
storageInfoP(storageTest, strNewFmt("repo128/test.file"), .ignoreMissing = true).exists, true, "test file exists");
TEST_RESULT_BOOL(
storageInfoP(
storageTest, strNewFmt("repo128/%s", strZ(latestLabel)), .ignoreMissing = true).exists, true,
"symlink to file exists");
// Verify link mapping via the local test storage
TEST_ASSIGN(
info,
storageInfoP(storageTest, strNewFmt("repo128/%s", strZ(latestLabel)), .ignoreMissing = false), "get symlink info");
TEST_RESULT_STR(info.linkDestination, testFile, "match link destination");
TEST_RESULT_INT(info.type, storageTypeLink, "check type is link");
// Verify file and link contents match
Buffer *testFileBuffer = storageGetP(storageNewReadP(storageTest, STRDEF("repo128/test.file")));
Buffer *symLinkBuffer = storageGetP(storageNewReadP(storageTest, strNewFmt("repo128/%s", strZ(latestLabel))));
TEST_RESULT_BOOL(bufEq(testFileBuffer, BUFSTRDEF("TESTME")), true, "file contents match test buffer");
TEST_RESULT_BOOL(bufEq(testFileBuffer, symLinkBuffer), true, "symlink and file contents match each other");
TEST_RESULT_VOID(storageRemoveP(storageRepoWrite, testFile), "remote remove test file");
TEST_RESULT_BOOL(
storageInfoP(storageTest, STRDEF("repo128/test.file"), .ignoreMissing = true).exists, false, "test file removed");
TEST_RESULT_VOID(storageRemoveP(storageRepoWrite, latestLabel), "remote remove symlink");
TEST_RESULT_BOOL(
storageInfoP(
storageTest, strNewFmt("repo128/%s", strZ(latestLabel)), .ignoreMissing = true).exists, false,
"symlink to file removed");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("hardlink success/fail");
// Create a file and hard link to it via the remote,
TEST_RESULT_VOID(storagePutP(storageNewWriteP(storageRepoWrite, testFile), BUFSTRDEF("TESTME")), "put test file");
TEST_RESULT_VOID(
storageLinkCreateP(
storageRepoWrite, testFile, strNewFmt("%s/%s", strZ(storagePathP(storageRepo, NULL)), strZ(latestLabel)),
.linkType = storageLinkHard),
"hardlink to test file");
// Check the repo via the local test storage to ensure the remote wrote to it, check that files and link match, then remove
// via remote and confirm removed
TEST_RESULT_BOOL(
storageInfoP(storageTest, STRDEF("repo128/test.file"), .ignoreMissing = true).exists, true, "test file exists");
TEST_RESULT_BOOL(
storageInfoP(
storageTest, strNewFmt("repo128/%s", strZ(latestLabel)), .ignoreMissing = true).exists, true, "hard link exists");
TEST_ASSIGN(
info,
storageInfoP(storageTest, strNewFmt("repo128/%s", strZ(latestLabel)), .ignoreMissing = false), "get hard link info");
TEST_RESULT_INT(info.type, storageTypeFile, "check type is file");
// Verify file and link contents match
testFileBuffer = storageGetP(storageNewReadP(storageTest, STRDEF("repo128/test.file")));
Buffer *hardLinkBuffer = storageGetP(storageNewReadP(storageTest, strNewFmt("repo128/%s", strZ(latestLabel))));
TEST_RESULT_BOOL(bufEq(testFileBuffer, BUFSTRDEF("TESTME")), true, "file contents match test buffer");
TEST_RESULT_BOOL(bufEq(testFileBuffer, hardLinkBuffer), true, "hard link and file contents match each other");
TEST_RESULT_VOID(storageRemoveP(storageRepoWrite, testFile), "remove test file");
TEST_RESULT_VOID(storageRemoveP(storageRepoWrite, latestLabel), "remove hard link");
TEST_RESULT_BOOL(
storageInfoP(storageTest, STRDEF("repo128/test.file"), .ignoreMissing = true).exists, false, "test file removed");
TEST_RESULT_BOOL(
storageInfoP(
storageTest, strNewFmt("repo128/%s", strZ(latestLabel)), .ignoreMissing = true).exists, false, "hard link removed");
// Hard link to directory not permitted
TEST_ERROR(
storageLinkCreateP(
storageRepoWrite,
strNewFmt("%s/%s", strZ(storagePathP(storageRepo, NULL)), strZ(path)),
strNewFmt("%s/%s", strZ(storagePathP(storageRepo, NULL)), strZ(latestLabel)), .linkType = storageLinkHard),
FileOpenError,
"raised from remote-0 shim protocol: unable to create hardlink '" TEST_PATH "/repo128/latest' to"
" '" TEST_PATH "/repo128/20181119-152138F': [1] Operation not permitted");
}
// When clients are freed by protocolClientFree() they do not wait for a response. We need to wait for a response here to be
// sure coverage data has been written by the remote. We also need to make sure that the mem context callback is cleared so that
// protocolClientFreeResource() will not be called and send another exit. protocolFree() is still required to free the client