mirror of
https://github.com/pgbackrest/pgbackrest.git
synced 2025-01-18 04:58:51 +02:00
Fix memory leak in file bundle backup/restore.
When converting restoreFile() to support file bundling in 34d64957 there were some I/O objects that were only freed at the end of the function that should have been freed at the end of each loop. Wrap the loops in temp mem contexts to fix this. Do the same to backupFile() since it would have a similar leak when resuming a backup. Since file bundles cannot be resumed the leak would not be as severe, but still seems worth doing to protect against future leaks.
This commit is contained in:
parent
d50a4442e4
commit
cd8db7d9e5
@ -15,6 +15,24 @@
|
||||
|
||||
<release-list>
|
||||
<release date="XXXX-XX-XX" version="2.42dev" title="UNDER DEVELOPMENT">
|
||||
<release-core-list>
|
||||
<release-bug-list>
|
||||
<release-item>
|
||||
<github-issue id="1877"/>
|
||||
<github-pull-request id="1878"/>
|
||||
|
||||
<release-item-contributor-list>
|
||||
<release-item-ideator id="MannerMan"/>
|
||||
<release-item-contributor id="david.steele"/>
|
||||
<release-item-reviewer id="john.morris"/>
|
||||
<!-- Actually tester, but we don't have a tag for that yet -->
|
||||
<release-item-reviewer id="MannerMan"/>
|
||||
</release-item-contributor-list>
|
||||
|
||||
<p>Fix memory leak in file bundle <cmd>backup</cmd>/<cmd>restore</cmd>.</p>
|
||||
</release-item>
|
||||
</release-bug-list>
|
||||
</release-core-list>
|
||||
</release>
|
||||
|
||||
<release date="2022-09-19" version="2.41" title="Backup Annotations">
|
||||
|
@ -65,129 +65,134 @@ backupFile(
|
||||
// Check files to determine which ones need to be copied
|
||||
for (unsigned int fileIdx = 0; fileIdx < lstSize(fileList); fileIdx++)
|
||||
{
|
||||
const BackupFile *const file = lstGet(fileList, fileIdx);
|
||||
ASSERT(file->pgFile != NULL);
|
||||
ASSERT(file->manifestFile != NULL);
|
||||
|
||||
BackupFileResult *const fileResult = lstAdd(
|
||||
result, &(BackupFileResult){.manifestFile = file->manifestFile, .backupCopyResult = backupCopyResultCopy});
|
||||
|
||||
// If checksum is defined then the file needs to be checked. If delta option then check the DB and possibly the repo,
|
||||
// else just check the repo.
|
||||
if (file->pgFileChecksum != NULL)
|
||||
// Use a per-file mem context to reduce memory usage
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// Does the file in pg match the checksum and size passed?
|
||||
bool pgFileMatch = false;
|
||||
const BackupFile *const file = lstGet(fileList, fileIdx);
|
||||
ASSERT(file->pgFile != NULL);
|
||||
ASSERT(file->manifestFile != NULL);
|
||||
|
||||
// If delta, then check the DB checksum and possibly the repo. If the checksum does not match in either case then
|
||||
// recopy.
|
||||
if (delta)
|
||||
BackupFileResult *const fileResult = lstAdd(
|
||||
result, &(BackupFileResult){.manifestFile = file->manifestFile, .backupCopyResult = backupCopyResultCopy});
|
||||
|
||||
// If checksum is defined then the file needs to be checked. If delta option then check the DB and possibly the
|
||||
// repo, else just check the repo.
|
||||
if (file->pgFileChecksum != NULL)
|
||||
{
|
||||
// Generate checksum/size for the pg file. Only read as many bytes as passed in pgFileSize. If the file has
|
||||
// grown since the manifest was built we don't need to consider the extra bytes since they will be replayed from
|
||||
// WAL during recovery.
|
||||
IoRead *read = storageReadIo(
|
||||
storageNewReadP(
|
||||
storagePg(), file->pgFile, .ignoreMissing = file->pgFileIgnoreMissing,
|
||||
.limit = file->pgFileCopyExactSize ? VARUINT64(file->pgFileSize) : NULL));
|
||||
ioFilterGroupAdd(ioReadFilterGroup(read), cryptoHashNew(hashTypeSha1));
|
||||
ioFilterGroupAdd(ioReadFilterGroup(read), ioSizeNew());
|
||||
// Does the file in pg match the checksum and size passed?
|
||||
bool pgFileMatch = false;
|
||||
|
||||
// If the pg file exists check the checksum/size
|
||||
if (ioReadDrain(read))
|
||||
// If delta, then check the DB checksum and possibly the repo. If the checksum does not match in either case
|
||||
// then recopy.
|
||||
if (delta)
|
||||
{
|
||||
const String *pgTestChecksum = pckReadStrP(
|
||||
ioFilterGroupResultP(ioReadFilterGroup(read), CRYPTO_HASH_FILTER_TYPE));
|
||||
uint64_t pgTestSize = pckReadU64P(ioFilterGroupResultP(ioReadFilterGroup(read), SIZE_FILTER_TYPE));
|
||||
// Generate checksum/size for the pg file. Only read as many bytes as passed in pgFileSize. If the file has
|
||||
// grown since the manifest was built we don't need to consider the extra bytes since they will be replayed
|
||||
// from WAL during recovery.
|
||||
IoRead *read = storageReadIo(
|
||||
storageNewReadP(
|
||||
storagePg(), file->pgFile, .ignoreMissing = file->pgFileIgnoreMissing,
|
||||
.limit = file->pgFileCopyExactSize ? VARUINT64(file->pgFileSize) : NULL));
|
||||
ioFilterGroupAdd(ioReadFilterGroup(read), cryptoHashNew(hashTypeSha1));
|
||||
ioFilterGroupAdd(ioReadFilterGroup(read), ioSizeNew());
|
||||
|
||||
// Does the pg file match?
|
||||
if (file->pgFileSize == pgTestSize && strEq(file->pgFileChecksum, pgTestChecksum))
|
||||
// If the pg file exists check the checksum/size
|
||||
if (ioReadDrain(read))
|
||||
{
|
||||
pgFileMatch = true;
|
||||
|
||||
// If it matches and is a reference to a previous backup then no need to copy the file
|
||||
if (file->manifestFileHasReference)
|
||||
{
|
||||
MEM_CONTEXT_BEGIN(lstMemContext(result))
|
||||
{
|
||||
fileResult->backupCopyResult = backupCopyResultNoOp;
|
||||
fileResult->copySize = pgTestSize;
|
||||
fileResult->copyChecksum = strDup(pgTestChecksum);
|
||||
}
|
||||
MEM_CONTEXT_END();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Else the source file is missing from the database so skip this file
|
||||
else
|
||||
fileResult->backupCopyResult = backupCopyResultSkip;
|
||||
}
|
||||
|
||||
// If this is not a delta backup or it is and the file exists and the checksum from the DB matches, then also test
|
||||
// the checksum of the file in the repo (unless it is in a prior backup) and if the checksum doesn't match, then
|
||||
// there may be corruption in the repo, so recopy
|
||||
if (!delta || !file->manifestFileHasReference)
|
||||
{
|
||||
// If this is a delta backup and the file is missing from the DB, then remove it from the repo
|
||||
// (backupManifestUpdate will remove it from the manifest)
|
||||
if (fileResult->backupCopyResult == backupCopyResultSkip)
|
||||
{
|
||||
storageRemoveP(storageRepoWrite(), repoFile);
|
||||
}
|
||||
else if (!delta || pgFileMatch)
|
||||
{
|
||||
// Check the repo file in a try block because on error (e.g. missing or corrupt file that can't be decrypted
|
||||
// or decompressed) we should recopy rather than ending the backup.
|
||||
TRY_BEGIN()
|
||||
{
|
||||
// Generate checksum/size for the repo file
|
||||
IoRead *read = storageReadIo(storageNewReadP(storageRepo(), repoFile));
|
||||
|
||||
if (cipherType != cipherTypeNone)
|
||||
{
|
||||
ioFilterGroupAdd(
|
||||
ioReadFilterGroup(read),
|
||||
cipherBlockNew(cipherModeDecrypt, cipherType, BUFSTR(cipherPass), NULL));
|
||||
}
|
||||
|
||||
// Decompress the file if compressed
|
||||
if (repoFileCompressType != compressTypeNone)
|
||||
ioFilterGroupAdd(ioReadFilterGroup(read), decompressFilter(repoFileCompressType));
|
||||
|
||||
ioFilterGroupAdd(ioReadFilterGroup(read), cryptoHashNew(hashTypeSha1));
|
||||
ioFilterGroupAdd(ioReadFilterGroup(read), ioSizeNew());
|
||||
|
||||
ioReadDrain(read);
|
||||
|
||||
// Test checksum/size
|
||||
const String *pgTestChecksum = pckReadStrP(
|
||||
ioFilterGroupResultP(ioReadFilterGroup(read), CRYPTO_HASH_FILTER_TYPE));
|
||||
uint64_t pgTestSize = pckReadU64P(ioFilterGroupResultP(ioReadFilterGroup(read), SIZE_FILTER_TYPE));
|
||||
|
||||
// No need to recopy if checksum/size match
|
||||
// Does the pg file match?
|
||||
if (file->pgFileSize == pgTestSize && strEq(file->pgFileChecksum, pgTestChecksum))
|
||||
{
|
||||
MEM_CONTEXT_BEGIN(lstMemContext(result))
|
||||
pgFileMatch = true;
|
||||
|
||||
// If it matches and is a reference to a previous backup then no need to copy the file
|
||||
if (file->manifestFileHasReference)
|
||||
{
|
||||
fileResult->backupCopyResult = backupCopyResultChecksum;
|
||||
fileResult->copySize = pgTestSize;
|
||||
fileResult->copyChecksum = strDup(pgTestChecksum);
|
||||
MEM_CONTEXT_BEGIN(lstMemContext(result))
|
||||
{
|
||||
fileResult->backupCopyResult = backupCopyResultNoOp;
|
||||
fileResult->copySize = pgTestSize;
|
||||
fileResult->copyChecksum = strDup(pgTestChecksum);
|
||||
}
|
||||
MEM_CONTEXT_END();
|
||||
}
|
||||
MEM_CONTEXT_END();
|
||||
}
|
||||
// Else recopy when repo file is not as expected
|
||||
else
|
||||
fileResult->backupCopyResult = backupCopyResultReCopy;
|
||||
}
|
||||
// Recopy on any kind of error
|
||||
CATCH_ANY()
|
||||
// Else the source file is missing from the database so skip this file
|
||||
else
|
||||
fileResult->backupCopyResult = backupCopyResultSkip;
|
||||
}
|
||||
|
||||
// If this is not a delta backup or it is and the file exists and the checksum from the DB matches, then also
|
||||
// test the checksum of the file in the repo (unless it is in a prior backup) and if the checksum doesn't match,
|
||||
// then there may be corruption in the repo, so recopy
|
||||
if (!delta || !file->manifestFileHasReference)
|
||||
{
|
||||
// If this is a delta backup and the file is missing from the DB, then remove it from the repo
|
||||
// (backupManifestUpdate will remove it from the manifest)
|
||||
if (fileResult->backupCopyResult == backupCopyResultSkip)
|
||||
{
|
||||
fileResult->backupCopyResult = backupCopyResultReCopy;
|
||||
storageRemoveP(storageRepoWrite(), repoFile);
|
||||
}
|
||||
else if (!delta || pgFileMatch)
|
||||
{
|
||||
// Check the repo file in a try block because on error (e.g. missing or corrupt file that can't be
|
||||
// decrypted or decompressed) we should recopy rather than ending the backup.
|
||||
TRY_BEGIN()
|
||||
{
|
||||
// Generate checksum/size for the repo file
|
||||
IoRead *read = storageReadIo(storageNewReadP(storageRepo(), repoFile));
|
||||
|
||||
if (cipherType != cipherTypeNone)
|
||||
{
|
||||
ioFilterGroupAdd(
|
||||
ioReadFilterGroup(read),
|
||||
cipherBlockNew(cipherModeDecrypt, cipherType, BUFSTR(cipherPass), NULL));
|
||||
}
|
||||
|
||||
// Decompress the file if compressed
|
||||
if (repoFileCompressType != compressTypeNone)
|
||||
ioFilterGroupAdd(ioReadFilterGroup(read), decompressFilter(repoFileCompressType));
|
||||
|
||||
ioFilterGroupAdd(ioReadFilterGroup(read), cryptoHashNew(hashTypeSha1));
|
||||
ioFilterGroupAdd(ioReadFilterGroup(read), ioSizeNew());
|
||||
|
||||
ioReadDrain(read);
|
||||
|
||||
// Test checksum/size
|
||||
const String *pgTestChecksum = pckReadStrP(
|
||||
ioFilterGroupResultP(ioReadFilterGroup(read), CRYPTO_HASH_FILTER_TYPE));
|
||||
uint64_t pgTestSize = pckReadU64P(ioFilterGroupResultP(ioReadFilterGroup(read), SIZE_FILTER_TYPE));
|
||||
|
||||
// No need to recopy if checksum/size match
|
||||
if (file->pgFileSize == pgTestSize && strEq(file->pgFileChecksum, pgTestChecksum))
|
||||
{
|
||||
MEM_CONTEXT_BEGIN(lstMemContext(result))
|
||||
{
|
||||
fileResult->backupCopyResult = backupCopyResultChecksum;
|
||||
fileResult->copySize = pgTestSize;
|
||||
fileResult->copyChecksum = strDup(pgTestChecksum);
|
||||
}
|
||||
MEM_CONTEXT_END();
|
||||
}
|
||||
// Else recopy when repo file is not as expected
|
||||
else
|
||||
fileResult->backupCopyResult = backupCopyResultReCopy;
|
||||
}
|
||||
// Recopy on any kind of error
|
||||
CATCH_ANY()
|
||||
{
|
||||
fileResult->backupCopyResult = backupCopyResultReCopy;
|
||||
}
|
||||
TRY_END();
|
||||
}
|
||||
TRY_END();
|
||||
}
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
}
|
||||
|
||||
// Are the files compressible during the copy?
|
||||
@ -199,97 +204,104 @@ backupFile(
|
||||
|
||||
for (unsigned int fileIdx = 0; fileIdx < lstSize(fileList); fileIdx++)
|
||||
{
|
||||
const BackupFile *const file = lstGet(fileList, fileIdx);
|
||||
BackupFileResult *const fileResult = lstGet(result, fileIdx);
|
||||
|
||||
if (fileResult->backupCopyResult == backupCopyResultCopy || fileResult->backupCopyResult == backupCopyResultReCopy)
|
||||
// Use a per-file mem context to reduce memory usage
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// Setup pg file for read. Only read as many bytes as passed in pgFileSize. If the file is growing it does no good
|
||||
// to copy data past the end of the size recorded in the manifest since those blocks will need to be replayed from
|
||||
// WAL during recovery.
|
||||
StorageRead *read = storageNewReadP(
|
||||
storagePg(), file->pgFile, .ignoreMissing = file->pgFileIgnoreMissing, .compressible = compressible,
|
||||
.limit = file->pgFileCopyExactSize ? VARUINT64(file->pgFileSize) : NULL);
|
||||
ioFilterGroupAdd(ioReadFilterGroup(storageReadIo(read)), cryptoHashNew(hashTypeSha1));
|
||||
ioFilterGroupAdd(ioReadFilterGroup(storageReadIo(read)), ioSizeNew());
|
||||
const BackupFile *const file = lstGet(fileList, fileIdx);
|
||||
BackupFileResult *const fileResult = lstGet(result, fileIdx);
|
||||
|
||||
// Add page checksum filter
|
||||
if (file->pgFileChecksumPage)
|
||||
if (fileResult->backupCopyResult == backupCopyResultCopy || fileResult->backupCopyResult == backupCopyResultReCopy)
|
||||
{
|
||||
ioFilterGroupAdd(
|
||||
ioReadFilterGroup(storageReadIo(read)),
|
||||
pageChecksumNew(
|
||||
segmentNumber(file->pgFile), PG_SEGMENT_PAGE_DEFAULT, storagePathP(storagePg(), file->pgFile)));
|
||||
}
|
||||
// Setup pg file for read. Only read as many bytes as passed in pgFileSize. If the file is growing it does no
|
||||
// good to copy data past the end of the size recorded in the manifest since those blocks will need to be
|
||||
// replayed from WAL during recovery.
|
||||
StorageRead *read = storageNewReadP(
|
||||
storagePg(), file->pgFile, .ignoreMissing = file->pgFileIgnoreMissing, .compressible = compressible,
|
||||
.limit = file->pgFileCopyExactSize ? VARUINT64(file->pgFileSize) : NULL);
|
||||
ioFilterGroupAdd(ioReadFilterGroup(storageReadIo(read)), cryptoHashNew(hashTypeSha1));
|
||||
ioFilterGroupAdd(ioReadFilterGroup(storageReadIo(read)), ioSizeNew());
|
||||
|
||||
// Add compression
|
||||
if (repoFileCompressType != compressTypeNone)
|
||||
{
|
||||
ioFilterGroupAdd(
|
||||
ioReadFilterGroup(storageReadIo(read)), compressFilter(repoFileCompressType, repoFileCompressLevel));
|
||||
}
|
||||
// Add page checksum filter
|
||||
if (file->pgFileChecksumPage)
|
||||
{
|
||||
ioFilterGroupAdd(
|
||||
ioReadFilterGroup(storageReadIo(read)),
|
||||
pageChecksumNew(
|
||||
segmentNumber(file->pgFile), PG_SEGMENT_PAGE_DEFAULT, storagePathP(storagePg(), file->pgFile)));
|
||||
}
|
||||
|
||||
// If there is a cipher then add the encrypt filter
|
||||
if (cipherType != cipherTypeNone)
|
||||
{
|
||||
ioFilterGroupAdd(
|
||||
ioReadFilterGroup(storageReadIo(read)),
|
||||
cipherBlockNew(cipherModeEncrypt, cipherType, BUFSTR(cipherPass), NULL));
|
||||
}
|
||||
// Add compression
|
||||
if (repoFileCompressType != compressTypeNone)
|
||||
{
|
||||
ioFilterGroupAdd(
|
||||
ioReadFilterGroup(storageReadIo(read)), compressFilter(repoFileCompressType, repoFileCompressLevel));
|
||||
}
|
||||
|
||||
// Add size filter last to calculate repo size
|
||||
ioFilterGroupAdd(ioReadFilterGroup(storageReadIo(read)), ioSizeNew());
|
||||
// If there is a cipher then add the encrypt filter
|
||||
if (cipherType != cipherTypeNone)
|
||||
{
|
||||
ioFilterGroupAdd(
|
||||
ioReadFilterGroup(storageReadIo(read)),
|
||||
cipherBlockNew(cipherModeEncrypt, cipherType, BUFSTR(cipherPass), NULL));
|
||||
}
|
||||
|
||||
// Open the source and destination and copy the file
|
||||
if (ioReadOpen(storageReadIo(read)))
|
||||
{
|
||||
if (write == NULL)
|
||||
// Add size filter last to calculate repo size
|
||||
ioFilterGroupAdd(ioReadFilterGroup(storageReadIo(read)), ioSizeNew());
|
||||
|
||||
// Open the source and destination and copy the file
|
||||
if (ioReadOpen(storageReadIo(read)))
|
||||
{
|
||||
// Setup the repo file for write. There is no need to write the file atomically (e.g. via a temp file on
|
||||
// Posix) because checksums are tested on resume after a failed backup. The path does not need to be synced
|
||||
// for each file because all paths are synced at the end of the backup.
|
||||
write = storageNewWriteP(
|
||||
storageRepoWrite(), repoFile, .compressible = compressible, .noAtomic = true, .noSyncPath = true);
|
||||
ioWriteOpen(storageWriteIo(write));
|
||||
}
|
||||
|
||||
// Copy data from source to destination
|
||||
ioCopyP(storageReadIo(read), storageWriteIo(write));
|
||||
|
||||
// Close the source
|
||||
ioReadClose(storageReadIo(read));
|
||||
|
||||
MEM_CONTEXT_BEGIN(lstMemContext(result))
|
||||
{
|
||||
// Get sizes and checksum
|
||||
fileResult->copySize = pckReadU64P(
|
||||
ioFilterGroupResultP(ioReadFilterGroup(storageReadIo(read)), SIZE_FILTER_TYPE, .idx = 0));
|
||||
fileResult->bundleOffset = bundleOffset;
|
||||
fileResult->copyChecksum = strDup(
|
||||
pckReadStrP(ioFilterGroupResultP(ioReadFilterGroup(storageReadIo(read)), CRYPTO_HASH_FILTER_TYPE)));
|
||||
fileResult->repoSize = pckReadU64P(
|
||||
ioFilterGroupResultP(ioReadFilterGroup(storageReadIo(read)), SIZE_FILTER_TYPE, .idx = 1));
|
||||
|
||||
// Get results of page checksum validation
|
||||
if (file->pgFileChecksumPage)
|
||||
// for each file because all paths are synced at the end of the backup. It needs to be created in the prior
|
||||
// context because it will live longer than a single loop when more than one file is being written.
|
||||
if (write == NULL)
|
||||
{
|
||||
fileResult->pageChecksumResult = pckDup(
|
||||
ioFilterGroupResultPackP(ioReadFilterGroup(storageReadIo(read)), PAGE_CHECKSUM_FILTER_TYPE));
|
||||
MEM_CONTEXT_PRIOR_BEGIN()
|
||||
{
|
||||
write = storageNewWriteP(
|
||||
storageRepoWrite(), repoFile, .compressible = compressible, .noAtomic = true,
|
||||
.noSyncPath = true);
|
||||
ioWriteOpen(storageWriteIo(write));
|
||||
}
|
||||
MEM_CONTEXT_PRIOR_END();
|
||||
}
|
||||
|
||||
// Copy data from source to destination
|
||||
ioCopyP(storageReadIo(read), storageWriteIo(write));
|
||||
|
||||
// Close the source
|
||||
ioReadClose(storageReadIo(read));
|
||||
|
||||
MEM_CONTEXT_BEGIN(lstMemContext(result))
|
||||
{
|
||||
// Get sizes and checksum
|
||||
fileResult->copySize = pckReadU64P(
|
||||
ioFilterGroupResultP(ioReadFilterGroup(storageReadIo(read)), SIZE_FILTER_TYPE, .idx = 0));
|
||||
fileResult->bundleOffset = bundleOffset;
|
||||
fileResult->copyChecksum = strDup(
|
||||
pckReadStrP(ioFilterGroupResultP(ioReadFilterGroup(storageReadIo(read)), CRYPTO_HASH_FILTER_TYPE)));
|
||||
fileResult->repoSize = pckReadU64P(
|
||||
ioFilterGroupResultP(ioReadFilterGroup(storageReadIo(read)), SIZE_FILTER_TYPE, .idx = 1));
|
||||
|
||||
// Get results of page checksum validation
|
||||
if (file->pgFileChecksumPage)
|
||||
{
|
||||
fileResult->pageChecksumResult = pckDup(
|
||||
ioFilterGroupResultPackP(ioReadFilterGroup(storageReadIo(read)), PAGE_CHECKSUM_FILTER_TYPE));
|
||||
}
|
||||
}
|
||||
MEM_CONTEXT_END();
|
||||
|
||||
bundleOffset += fileResult->repoSize;
|
||||
}
|
||||
MEM_CONTEXT_END();
|
||||
|
||||
// Free the read object. This is very important if many files are being read because they can each contain a lot
|
||||
// of buffers.
|
||||
storageReadFree(read);
|
||||
|
||||
bundleOffset += fileResult->repoSize;
|
||||
// Else if source file is missing and the read setup indicated ignore a missing file, the database removed it so
|
||||
// skip it
|
||||
else
|
||||
fileResult->backupCopyResult = backupCopyResultSkip;
|
||||
}
|
||||
// Else if source file is missing and the read setup indicated ignore a missing file, the database removed it so
|
||||
// skip it
|
||||
else
|
||||
fileResult->backupCopyResult = backupCopyResultSkip;
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
}
|
||||
|
||||
// Close the repository file if it was opened
|
||||
|
@ -47,122 +47,133 @@ List *restoreFile(
|
||||
// Check files to determine which ones need to be restored
|
||||
for (unsigned int fileIdx = 0; fileIdx < lstSize(fileList); fileIdx++)
|
||||
{
|
||||
const RestoreFile *const file = lstGet(fileList, fileIdx);
|
||||
ASSERT(file->name != NULL);
|
||||
ASSERT(file->limit == NULL || varType(file->limit) == varTypeUInt64);
|
||||
|
||||
RestoreFileResult *const fileResult = lstAdd(
|
||||
result, &(RestoreFileResult){.manifestFile = file->manifestFile, .result = restoreResultCopy});
|
||||
|
||||
// Perform delta if requested. Delta zero-length files to avoid overwriting the file if the timestamp is correct.
|
||||
if (delta && !file->zero)
|
||||
// Use a per-file mem context to reduce memory usage
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// Perform delta if the file exists
|
||||
StorageInfo info = storageInfoP(storagePg(), file->name, .ignoreMissing = true, .followLink = true);
|
||||
const RestoreFile *const file = lstGet(fileList, fileIdx);
|
||||
ASSERT(file->name != NULL);
|
||||
ASSERT(file->limit == NULL || varType(file->limit) == varTypeUInt64);
|
||||
|
||||
if (info.exists)
|
||||
RestoreFileResult *const fileResult = lstAdd(
|
||||
result, &(RestoreFileResult){.manifestFile = file->manifestFile, .result = restoreResultCopy});
|
||||
|
||||
// Perform delta if requested. Delta zero-length files to avoid overwriting the file if the timestamp is correct.
|
||||
if (delta && !file->zero)
|
||||
{
|
||||
// If force then use size/timestamp delta
|
||||
if (deltaForce)
|
||||
// Perform delta if the file exists
|
||||
StorageInfo info = storageInfoP(storagePg(), file->name, .ignoreMissing = true, .followLink = true);
|
||||
|
||||
if (info.exists)
|
||||
{
|
||||
// Make sure that timestamp/size are equal and that timestamp is before the copy start time of the backup
|
||||
if (info.size == file->size && info.timeModified == file->timeModified && info.timeModified < copyTimeBegin)
|
||||
fileResult->result = restoreResultPreserve;
|
||||
}
|
||||
// Else use size and checksum
|
||||
else
|
||||
{
|
||||
// Only continue delta if the file size is as expected or larger
|
||||
if (info.size >= file->size)
|
||||
// If force then use size/timestamp delta
|
||||
if (deltaForce)
|
||||
{
|
||||
const char *const fileName = strZ(storagePathP(storagePg(), file->name));
|
||||
|
||||
// If the file was extended since the backup, then truncate it to the size it was during the backup as
|
||||
// it might have only been appended to with the earlier portion being unchanged (we will verify this
|
||||
// using the checksum below)
|
||||
if (info.size > file->size)
|
||||
// Make sure that timestamp/size are equal and the timestamp is before the copy start time of the backup
|
||||
if (info.size == file->size && info.timeModified == file->timeModified &&
|
||||
info.timeModified < copyTimeBegin)
|
||||
{
|
||||
// Open the file for write
|
||||
int fd = open(fileName, O_WRONLY, 0);
|
||||
THROW_ON_SYS_ERROR_FMT(fd == -1, FileReadError, STORAGE_ERROR_WRITE_OPEN, fileName);
|
||||
|
||||
TRY_BEGIN()
|
||||
{
|
||||
// Truncate to original size
|
||||
THROW_ON_SYS_ERROR_FMT(
|
||||
ftruncate(fd, (off_t)file->size) == -1, FileWriteError, "unable to truncate file '%s'",
|
||||
fileName);
|
||||
|
||||
// Sync
|
||||
THROW_ON_SYS_ERROR_FMT(fsync(fd) == -1, FileSyncError, STORAGE_ERROR_WRITE_SYNC, fileName);
|
||||
}
|
||||
FINALLY()
|
||||
{
|
||||
THROW_ON_SYS_ERROR_FMT(close(fd) == -1, FileCloseError, STORAGE_ERROR_WRITE_CLOSE, fileName);
|
||||
}
|
||||
TRY_END();
|
||||
|
||||
// Update info
|
||||
info = storageInfoP(storagePg(), file->name, .followLink = true);
|
||||
}
|
||||
|
||||
// Generate checksum for the file if size is not zero
|
||||
IoRead *read = NULL;
|
||||
|
||||
if (file->size != 0)
|
||||
{
|
||||
read = storageReadIo(storageNewReadP(storagePgWrite(), file->name));
|
||||
ioFilterGroupAdd(ioReadFilterGroup(read), cryptoHashNew(hashTypeSha1));
|
||||
ioReadDrain(read);
|
||||
}
|
||||
|
||||
// If the checksum is the same (or file is zero size) then no need to copy the file
|
||||
if (file->size == 0 ||
|
||||
strEq(
|
||||
file->checksum,
|
||||
pckReadStrP(ioFilterGroupResultP(ioReadFilterGroup(read), CRYPTO_HASH_FILTER_TYPE))))
|
||||
{
|
||||
// If the hash/size are now the same but the time is not, then set the time back to the backup time.
|
||||
// This helps with unit testing, but also presents a pristine version of the database after restore.
|
||||
if (info.timeModified != file->timeModified)
|
||||
{
|
||||
THROW_ON_SYS_ERROR_FMT(
|
||||
utime(
|
||||
fileName,
|
||||
&((struct utimbuf){.actime = file->timeModified, .modtime = file->timeModified})) == -1,
|
||||
FileInfoError, "unable to set time for '%s'", fileName);
|
||||
}
|
||||
|
||||
fileResult->result = restoreResultPreserve;
|
||||
}
|
||||
}
|
||||
// Else use size and checksum
|
||||
else
|
||||
{
|
||||
// Only continue delta if the file size is as expected or larger
|
||||
if (info.size >= file->size)
|
||||
{
|
||||
const char *const fileName = strZ(storagePathP(storagePg(), file->name));
|
||||
|
||||
// If the file was extended since the backup, then truncate it to the size it was during the backup
|
||||
// as it might have only been appended to with the earlier portion being unchanged (we will verify
|
||||
// this using the checksum below)
|
||||
if (info.size > file->size)
|
||||
{
|
||||
// Open the file for write
|
||||
int fd = open(fileName, O_WRONLY, 0);
|
||||
THROW_ON_SYS_ERROR_FMT(fd == -1, FileReadError, STORAGE_ERROR_WRITE_OPEN, fileName);
|
||||
|
||||
TRY_BEGIN()
|
||||
{
|
||||
// Truncate to original size
|
||||
THROW_ON_SYS_ERROR_FMT(
|
||||
ftruncate(fd, (off_t)file->size) == -1, FileWriteError, "unable to truncate file '%s'",
|
||||
fileName);
|
||||
|
||||
// Sync
|
||||
THROW_ON_SYS_ERROR_FMT(fsync(fd) == -1, FileSyncError, STORAGE_ERROR_WRITE_SYNC, fileName);
|
||||
}
|
||||
FINALLY()
|
||||
{
|
||||
THROW_ON_SYS_ERROR_FMT(
|
||||
close(fd) == -1, FileCloseError, STORAGE_ERROR_WRITE_CLOSE, fileName);
|
||||
}
|
||||
TRY_END();
|
||||
|
||||
// Update info
|
||||
info = storageInfoP(storagePg(), file->name, .followLink = true);
|
||||
}
|
||||
|
||||
// Generate checksum for the file if size is not zero
|
||||
IoRead *read = NULL;
|
||||
|
||||
if (file->size != 0)
|
||||
{
|
||||
read = storageReadIo(storageNewReadP(storagePgWrite(), file->name));
|
||||
ioFilterGroupAdd(ioReadFilterGroup(read), cryptoHashNew(hashTypeSha1));
|
||||
ioReadDrain(read);
|
||||
}
|
||||
|
||||
// If the checksum is the same (or file is zero size) then no need to copy the file
|
||||
if (file->size == 0 ||
|
||||
strEq(
|
||||
file->checksum,
|
||||
pckReadStrP(ioFilterGroupResultP(ioReadFilterGroup(read), CRYPTO_HASH_FILTER_TYPE))))
|
||||
{
|
||||
// If the hash/size are now the same but the time is not, then set the time back to the backup
|
||||
// time. This helps with unit testing, but also presents a pristine version of the database
|
||||
// after restore.
|
||||
if (info.timeModified != file->timeModified)
|
||||
{
|
||||
THROW_ON_SYS_ERROR_FMT(
|
||||
utime(
|
||||
fileName,
|
||||
&((struct utimbuf){
|
||||
.actime = file->timeModified, .modtime = file->timeModified})) == -1,
|
||||
FileInfoError, "unable to set time for '%s'", fileName);
|
||||
}
|
||||
|
||||
fileResult->result = restoreResultPreserve;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create zeroed and zero-length files
|
||||
if (fileResult->result == restoreResultCopy && (file->size == 0 || file->zero))
|
||||
{
|
||||
// Create destination file
|
||||
StorageWrite *pgFileWrite = storageNewWriteP(
|
||||
storagePgWrite(), file->name, .modeFile = file->mode, .user = file->user, .group = file->group,
|
||||
.timeModified = file->timeModified, .noAtomic = true, .noCreatePath = true, .noSyncPath = true);
|
||||
|
||||
ioWriteOpen(storageWriteIo(pgFileWrite));
|
||||
|
||||
// Truncate the file to specified length (note in this case the file will grow, not shrink)
|
||||
if (file->zero)
|
||||
// Create zeroed and zero-length files
|
||||
if (fileResult->result == restoreResultCopy && (file->size == 0 || file->zero))
|
||||
{
|
||||
THROW_ON_SYS_ERROR_FMT(
|
||||
ftruncate(ioWriteFd(storageWriteIo(pgFileWrite)), (off_t)file->size) == -1, FileWriteError,
|
||||
"unable to truncate '%s'", strZ(file->name));
|
||||
// Create destination file
|
||||
StorageWrite *pgFileWrite = storageNewWriteP(
|
||||
storagePgWrite(), file->name, .modeFile = file->mode, .user = file->user, .group = file->group,
|
||||
.timeModified = file->timeModified, .noAtomic = true, .noCreatePath = true, .noSyncPath = true);
|
||||
|
||||
ioWriteOpen(storageWriteIo(pgFileWrite));
|
||||
|
||||
// Truncate the file to specified length (note in this case the file will grow, not shrink)
|
||||
if (file->zero)
|
||||
{
|
||||
THROW_ON_SYS_ERROR_FMT(
|
||||
ftruncate(ioWriteFd(storageWriteIo(pgFileWrite)), (off_t)file->size) == -1, FileWriteError,
|
||||
"unable to truncate '%s'", strZ(file->name));
|
||||
}
|
||||
|
||||
ioWriteClose(storageWriteIo(pgFileWrite));
|
||||
|
||||
// Report the file as zeroed or zero-length
|
||||
fileResult->result = restoreResultZero;
|
||||
}
|
||||
|
||||
ioWriteClose(storageWriteIo(pgFileWrite));
|
||||
|
||||
// Report the file as zeroed or zero-length
|
||||
fileResult->result = restoreResultZero;
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
}
|
||||
|
||||
// Copy files from repository to database
|
||||
@ -171,97 +182,107 @@ List *restoreFile(
|
||||
|
||||
for (unsigned int fileIdx = 0; fileIdx < lstSize(fileList); fileIdx++)
|
||||
{
|
||||
const RestoreFile *const file = lstGet(fileList, fileIdx);
|
||||
const RestoreFileResult *const fileResult = lstGet(result, fileIdx);
|
||||
|
||||
// Copy file from repository to database
|
||||
if (fileResult->result == restoreResultCopy)
|
||||
// Use a per-file mem context to reduce memory usage
|
||||
MEM_CONTEXT_TEMP_BEGIN()
|
||||
{
|
||||
// If no repo file is currently open
|
||||
if (repoFileLimit == 0)
|
||||
const RestoreFile *const file = lstGet(fileList, fileIdx);
|
||||
const RestoreFileResult *const fileResult = lstGet(result, fileIdx);
|
||||
|
||||
// Copy file from repository to database
|
||||
if (fileResult->result == restoreResultCopy)
|
||||
{
|
||||
// If a limit is specified then we need to use it, even if there is only one pg file to copy, because we might
|
||||
// be reading from the middle of a repo file containing many pg files
|
||||
if (file->limit != NULL)
|
||||
// If no repo file is currently open
|
||||
if (repoFileLimit == 0)
|
||||
{
|
||||
ASSERT(varUInt64(file->limit) != 0);
|
||||
repoFileLimit = varUInt64(file->limit);
|
||||
|
||||
// Determine how many files can be copied with one read
|
||||
for (unsigned int fileNextIdx = fileIdx + 1; fileNextIdx < lstSize(fileList); fileNextIdx++)
|
||||
// If a limit is specified then we need to use it, even if there is only one pg file to copy, because we
|
||||
// might be reading from the middle of a repo file containing many pg files
|
||||
if (file->limit != NULL)
|
||||
{
|
||||
// Only files that are being copied are considered
|
||||
if (((const RestoreFileResult *)lstGet(result, fileNextIdx))->result == restoreResultCopy)
|
||||
ASSERT(varUInt64(file->limit) != 0);
|
||||
repoFileLimit = varUInt64(file->limit);
|
||||
|
||||
// Determine how many files can be copied with one read
|
||||
for (unsigned int fileNextIdx = fileIdx + 1; fileNextIdx < lstSize(fileList); fileNextIdx++)
|
||||
{
|
||||
const RestoreFile *const fileNext = lstGet(fileList, fileNextIdx);
|
||||
ASSERT(fileNext->limit != NULL && varUInt64(fileNext->limit) != 0);
|
||||
// Only files that are being copied are considered
|
||||
if (((const RestoreFileResult *)lstGet(result, fileNextIdx))->result == restoreResultCopy)
|
||||
{
|
||||
const RestoreFile *const fileNext = lstGet(fileList, fileNextIdx);
|
||||
ASSERT(fileNext->limit != NULL && varUInt64(fileNext->limit) != 0);
|
||||
|
||||
// Break if the offset is not the first file's offset + the limit of all additional files so far
|
||||
if (fileNext->offset != file->offset + repoFileLimit)
|
||||
// Break if the offset is not the first file's offset + the limit of all additional files so far
|
||||
if (fileNext->offset != file->offset + repoFileLimit)
|
||||
break;
|
||||
|
||||
repoFileLimit += varUInt64(fileNext->limit);
|
||||
}
|
||||
// Else if the file was not copied then there is a gap so break
|
||||
else
|
||||
break;
|
||||
|
||||
repoFileLimit += varUInt64(fileNext->limit);
|
||||
}
|
||||
// Else if the file was not copied then there is a gap so break
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
// Create and open the repo file. It needs to be created in the prior context because it will live longer
|
||||
// than a single loop when more than one file is being read.
|
||||
MEM_CONTEXT_PRIOR_BEGIN()
|
||||
{
|
||||
repoFileRead = storageNewReadP(
|
||||
storageRepoIdx(repoIdx), repoFile,
|
||||
.compressible = repoFileCompressType == compressTypeNone && cipherPass == NULL,
|
||||
.offset = file->offset, .limit = repoFileLimit != 0 ? VARUINT64(repoFileLimit) : NULL);
|
||||
ioReadOpen(storageReadIo(repoFileRead));
|
||||
}
|
||||
MEM_CONTEXT_PRIOR_END();
|
||||
}
|
||||
|
||||
// Create and open the repo file
|
||||
repoFileRead = storageNewReadP(
|
||||
storageRepoIdx(repoIdx), repoFile,
|
||||
.compressible = repoFileCompressType == compressTypeNone && cipherPass == NULL, .offset = file->offset,
|
||||
.limit = repoFileLimit != 0 ? VARUINT64(repoFileLimit) : NULL);
|
||||
ioReadOpen(storageReadIo(repoFileRead));
|
||||
// Create pg file
|
||||
StorageWrite *pgFileWrite = storageNewWriteP(
|
||||
storagePgWrite(), file->name, .modeFile = file->mode, .user = file->user, .group = file->group,
|
||||
.timeModified = file->timeModified, .noAtomic = true, .noCreatePath = true, .noSyncPath = true);
|
||||
|
||||
IoFilterGroup *filterGroup = ioWriteFilterGroup(storageWriteIo(pgFileWrite));
|
||||
|
||||
// Add decryption filter
|
||||
if (cipherPass != NULL)
|
||||
{
|
||||
ioFilterGroupAdd(
|
||||
filterGroup, cipherBlockNew(cipherModeDecrypt, cipherTypeAes256Cbc, BUFSTR(cipherPass), NULL));
|
||||
}
|
||||
|
||||
// Add decompression filter
|
||||
if (repoFileCompressType != compressTypeNone)
|
||||
ioFilterGroupAdd(filterGroup, decompressFilter(repoFileCompressType));
|
||||
|
||||
// Add sha1 filter
|
||||
ioFilterGroupAdd(filterGroup, cryptoHashNew(hashTypeSha1));
|
||||
|
||||
// Add size filter
|
||||
ioFilterGroupAdd(filterGroup, ioSizeNew());
|
||||
|
||||
// Copy file
|
||||
ioWriteOpen(storageWriteIo(pgFileWrite));
|
||||
ioCopyP(storageReadIo(repoFileRead), storageWriteIo(pgFileWrite), .limit = file->limit);
|
||||
ioWriteClose(storageWriteIo(pgFileWrite));
|
||||
|
||||
// If more than one file is being copied from a single read then decrement the limit
|
||||
if (repoFileLimit != 0)
|
||||
repoFileLimit -= varUInt64(file->limit);
|
||||
|
||||
// Free the repo file when there are no more files to copy from it
|
||||
if (repoFileLimit == 0)
|
||||
storageReadFree(repoFileRead);
|
||||
|
||||
// Validate checksum
|
||||
if (!strEq(file->checksum, pckReadStrP(ioFilterGroupResultP(filterGroup, CRYPTO_HASH_FILTER_TYPE))))
|
||||
{
|
||||
THROW_FMT(
|
||||
ChecksumError,
|
||||
"error restoring '%s': actual checksum '%s' does not match expected checksum '%s'", strZ(file->name),
|
||||
strZ(pckReadStrP(ioFilterGroupResultP(filterGroup, CRYPTO_HASH_FILTER_TYPE))), strZ(file->checksum));
|
||||
}
|
||||
}
|
||||
|
||||
// Create pg file
|
||||
StorageWrite *pgFileWrite = storageNewWriteP(
|
||||
storagePgWrite(), file->name, .modeFile = file->mode, .user = file->user, .group = file->group,
|
||||
.timeModified = file->timeModified, .noAtomic = true, .noCreatePath = true, .noSyncPath = true);
|
||||
|
||||
IoFilterGroup *filterGroup = ioWriteFilterGroup(storageWriteIo(pgFileWrite));
|
||||
|
||||
// Add decryption filter
|
||||
if (cipherPass != NULL)
|
||||
ioFilterGroupAdd(filterGroup, cipherBlockNew(cipherModeDecrypt, cipherTypeAes256Cbc, BUFSTR(cipherPass), NULL));
|
||||
|
||||
// Add decompression filter
|
||||
if (repoFileCompressType != compressTypeNone)
|
||||
ioFilterGroupAdd(filterGroup, decompressFilter(repoFileCompressType));
|
||||
|
||||
// Add sha1 filter
|
||||
ioFilterGroupAdd(filterGroup, cryptoHashNew(hashTypeSha1));
|
||||
|
||||
// Add size filter
|
||||
ioFilterGroupAdd(filterGroup, ioSizeNew());
|
||||
|
||||
// Copy file
|
||||
ioWriteOpen(storageWriteIo(pgFileWrite));
|
||||
ioCopyP(storageReadIo(repoFileRead), storageWriteIo(pgFileWrite), .limit = file->limit);
|
||||
ioWriteClose(storageWriteIo(pgFileWrite));
|
||||
|
||||
// If more than one file is being copied from a single read then decrement the limit
|
||||
if (repoFileLimit != 0)
|
||||
repoFileLimit -= varUInt64(file->limit);
|
||||
|
||||
// Free the repo file when there are no more files to copy from it
|
||||
if (repoFileLimit == 0)
|
||||
storageReadFree(repoFileRead);
|
||||
|
||||
// Validate checksum
|
||||
if (!strEq(file->checksum, pckReadStrP(ioFilterGroupResultP(filterGroup, CRYPTO_HASH_FILTER_TYPE))))
|
||||
{
|
||||
THROW_FMT(
|
||||
ChecksumError,
|
||||
"error restoring '%s': actual checksum '%s' does not match expected checksum '%s'", strZ(file->name),
|
||||
strZ(pckReadStrP(ioFilterGroupResultP(filterGroup, CRYPTO_HASH_FILTER_TYPE))), strZ(file->checksum));
|
||||
}
|
||||
|
||||
// Free the pg file
|
||||
storageWriteFree(pgFileWrite);
|
||||
}
|
||||
MEM_CONTEXT_TEMP_END();
|
||||
}
|
||||
|
||||
lstMove(result, memContextPrior());
|
||||
|
Loading…
x
Reference in New Issue
Block a user