Skip to content

Commit 1aac67b

Browse files
committed
Download to temporary .s3tmp files with atomic rename on validation success
1 parent a5262e5 commit 1aac67b

File tree

3 files changed

+95
-33
lines changed

3 files changed

+95
-33
lines changed

src/aws-cpp-sdk-transfer/include/aws/transfer/TransferHandle.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,12 @@ namespace Aws
291291
*/
292292
inline const Aws::String& GetTargetFilePath() const { return m_fileName; }
293293

294+
/**
295+
* (Download only) Temporary file path used during download before atomic rename to target.
296+
*/
297+
inline const Aws::String& GetTempFilePath() const { return m_tempFilePath; }
298+
inline void SetTempFilePath(const Aws::String& tempPath) { m_tempFilePath = tempPath; }
299+
294300
/**
295301
* (Download only) version id of the object to retrieve; if not specified in constructor, then latest is used
296302
*/
@@ -366,6 +372,11 @@ namespace Aws
366372
void WaitUntilFinished() const;
367373

368374
const CreateDownloadStreamCallback& GetCreateDownloadStreamFunction() const { return m_createDownloadStreamFn; }
375+
void SetCreateDownloadStreamFunction(const CreateDownloadStreamCallback& fn) {
376+
std::lock_guard<std::mutex> lock(m_downloadStreamLock);
377+
m_createDownloadStreamFn = fn;
378+
m_downloadStream = nullptr;
379+
}
369380

370381
/**
371382
* Write @partStream to the configured output (f)stream.
@@ -409,6 +420,7 @@ namespace Aws
409420
Aws::String m_bucket;
410421
Aws::String m_key;
411422
Aws::String m_fileName;
423+
Aws::String m_tempFilePath;
412424
Aws::String m_contentType;
413425
Aws::String m_versionId;
414426
Aws::String m_etag;

src/aws-cpp-sdk-transfer/include/aws/transfer/TransferManager.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,9 +341,9 @@ namespace Aws
341341
/**
342342
* Validates downloaded file checksum against expected checksum from HeadObject.
343343
* @param handle The transfer handle containing checksum and file path.
344-
* @return True if validation passes or no checksum to validate, false if validation fails.
344+
* @return Success outcome if validation passes or no checksum to validate, error outcome if validation fails.
345345
*/
346-
bool ValidateDownloadChecksum(const std::shared_ptr<TransferHandle>& handle);
346+
Aws::Utils::Outcome<Aws::NoResult, Aws::Client::AWSError<Aws::S3::S3Errors>> ValidateDownloadChecksum(const std::shared_ptr<TransferHandle>& handle);
347347

348348
static Aws::String DetermineFilePath(const Aws::String& directory, const Aws::String& prefix, const Aws::String& keyName);
349349

src/aws-cpp-sdk-transfer/source/transfer/TransferManager.cpp

Lines changed: 81 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ namespace Aws
3636
return (path.find_last_of('/') == path.size() - 1 || path.find_last_of('\\') == path.size() - 1);
3737
}
3838

39+
static Aws::String GenerateTempFilePath(const Aws::String& targetPath)
40+
{
41+
static const char* HEX_CHARS = "0123456789abcdef";
42+
char uniqueId[9] = {0};
43+
for (int i = 0; i < 8; ++i) {
44+
uniqueId[i] = HEX_CHARS[rand() % 16];
45+
}
46+
47+
size_t lastSlash = targetPath.find_last_of("/\\");
48+
if (lastSlash != Aws::String::npos) {
49+
return targetPath.substr(0, lastSlash + 1) + ".s3tmp." + uniqueId;
50+
}
51+
return ".s3tmp." + Aws::String(uniqueId);
52+
}
53+
3954
template <typename RequestT>
4055
static void SetChecksumOnRequest(RequestT& request, S3::Model::ChecksumAlgorithm checksumAlgorithm, const Aws::String& checksum) {
4156
if (checksumAlgorithm == S3::Model::ChecksumAlgorithm::CRC64NVME) {
@@ -231,15 +246,19 @@ namespace Aws
231246
const DownloadConfiguration& downloadConfig,
232247
const std::shared_ptr<const Aws::Client::AsyncCallerContext>& context)
233248
{
249+
Aws::String tempFilePath = GenerateTempFilePath(writeToFile);
250+
234251
#ifdef _MSC_VER
235-
auto createFileFn = [=]() { return Aws::New<Aws::FStream>(CLASS_TAG, Aws::Utils::StringUtils::ToWString(writeToFile.c_str()).c_str(),
252+
auto createFileFn = [=]() { return Aws::New<Aws::FStream>(CLASS_TAG, Aws::Utils::StringUtils::ToWString(tempFilePath.c_str()).c_str(),
236253
std::ios_base::out | std::ios_base::in | std::ios_base::binary | std::ios_base::trunc);};
237254
#else
238-
auto createFileFn = [=]() { return Aws::New<Aws::FStream>(CLASS_TAG, writeToFile.c_str(),
255+
auto createFileFn = [=]() { return Aws::New<Aws::FStream>(CLASS_TAG, tempFilePath.c_str(),
239256
std::ios_base::out | std::ios_base::in | std::ios_base::binary | std::ios_base::trunc);};
240257
#endif
241258

242-
return DownloadFile(bucketName, keyName, createFileFn, downloadConfig, writeToFile, context);
259+
auto handle = DownloadFile(bucketName, keyName, createFileFn, downloadConfig, writeToFile, context);
260+
handle->SetTempFilePath(tempFilePath);
261+
return handle;
243262
}
244263

245264
std::shared_ptr<TransferHandle> TransferManager::RetryUpload(const Aws::String& fileName, const std::shared_ptr<TransferHandle>& retryHandle)
@@ -939,16 +958,12 @@ namespace Aws
939958
handle->ChangePartToCompleted(partState, getObjectOutcome.GetResult().GetETag());
940959
getObjectOutcome.GetResult().GetBody().flush();
941960

942-
if (!ValidateDownloadChecksum(handle)) {
943-
Aws::Client::AWSError<Aws::S3::S3Errors> checksumError(
944-
Aws::S3::S3Errors::INTERNAL_FAILURE,
945-
"ChecksumMismatch",
946-
"Downloaded file checksum does not match expected checksum",
947-
false);
961+
auto checksumOutcome = ValidateDownloadChecksum(handle);
962+
if (!checksumOutcome.IsSuccess()) {
948963
handle->ChangePartToFailed(partState);
949964
handle->UpdateStatus(TransferStatus::FAILED);
950-
handle->SetError(checksumError);
951-
TriggerErrorCallback(handle, checksumError);
965+
handle->SetError(checksumOutcome.GetError());
966+
TriggerErrorCallback(handle, checksumOutcome.GetError());
952967
TriggerTransferStatusUpdatedCallback(handle);
953968
return;
954969
}
@@ -961,7 +976,12 @@ namespace Aws
961976
<< "] Failed to download object to Bucket: [" << handle->GetBucketName() << "] with Key: ["
962977
<< handle->GetKey() << "] " << getObjectOutcome.GetError());
963978
handle->ChangePartToFailed(partState);
964-
handle->UpdateStatus(DetermineIfFailedOrCanceled(*handle));
979+
auto finalStatus = DetermineIfFailedOrCanceled(*handle);
980+
handle->UpdateStatus(finalStatus);
981+
982+
if (finalStatus == TransferStatus::FAILED && !handle->GetTempFilePath().empty()) {
983+
Aws::FileSystem::RemoveFileIfExists(handle->GetTempFilePath().c_str());
984+
}
965985
handle->SetError(getObjectOutcome.GetError());
966986

967987
TriggerErrorCallback(handle, getObjectOutcome.GetError());
@@ -1271,16 +1291,11 @@ namespace Aws
12711291
{
12721292
outcome.GetResult().GetBody().flush();
12731293

1274-
// Validate checksum if available
1275-
if (!ValidateDownloadChecksum(handle)) {
1276-
Aws::Client::AWSError<Aws::S3::S3Errors> checksumError(
1277-
Aws::S3::S3Errors::INTERNAL_FAILURE,
1278-
"ChecksumMismatch",
1279-
"Downloaded file checksum does not match expected checksum",
1280-
false);
1294+
auto checksumOutcome = ValidateDownloadChecksum(handle);
1295+
if (!checksumOutcome.IsSuccess()) {
12811296
handle->UpdateStatus(TransferStatus::FAILED);
1282-
handle->SetError(checksumError);
1283-
TriggerErrorCallback(handle, checksumError);
1297+
handle->SetError(checksumOutcome.GetError());
1298+
TriggerErrorCallback(handle, checksumOutcome.GetError());
12841299
TriggerTransferStatusUpdatedCallback(handle);
12851300
partState->SetDownloadPartStream(nullptr);
12861301
return;
@@ -1290,7 +1305,12 @@ namespace Aws
12901305
}
12911306
else
12921307
{
1293-
handle->UpdateStatus(DetermineIfFailedOrCanceled(*handle));
1308+
auto finalStatus = DetermineIfFailedOrCanceled(*handle);
1309+
handle->UpdateStatus(finalStatus);
1310+
1311+
if (finalStatus == TransferStatus::FAILED && !handle->GetTempFilePath().empty()) {
1312+
Aws::FileSystem::RemoveFileIfExists(handle->GetTempFilePath().c_str());
1313+
}
12941314
}
12951315
TriggerTransferStatusUpdatedCallback(handle);
12961316
}
@@ -1641,9 +1661,21 @@ namespace Aws
16411661
}
16421662
}
16431663

1644-
bool TransferManager::ValidateDownloadChecksum(const std::shared_ptr<TransferHandle>& handle) {
1645-
if (handle->GetChecksum().empty() || handle->GetTargetFilePath().empty()) {
1646-
return true;
1664+
Aws::Utils::Outcome<Aws::NoResult, Aws::Client::AWSError<Aws::S3::S3Errors>> TransferManager::ValidateDownloadChecksum(const std::shared_ptr<TransferHandle>& handle) {
1665+
if (handle->GetChecksum().empty()) {
1666+
if (!handle->GetTempFilePath().empty() && !handle->GetTargetFilePath().empty()) {
1667+
if (!Aws::FileSystem::RelocateFileOrDirectory(handle->GetTempFilePath().c_str(), handle->GetTargetFilePath().c_str())) {
1668+
AWS_LOGSTREAM_ERROR(CLASS_TAG, "Failed to rename temp file from " << handle->GetTempFilePath() << " to " << handle->GetTargetFilePath());
1669+
Aws::FileSystem::RemoveFileIfExists(handle->GetTempFilePath().c_str());
1670+
return Aws::Client::AWSError<Aws::S3::S3Errors>(Aws::S3::S3Errors::INTERNAL_FAILURE, "FileRenameError", "Failed to rename temporary file to target", false);
1671+
}
1672+
}
1673+
return Aws::NoResult();
1674+
}
1675+
1676+
Aws::String fileToValidate = handle->GetTempFilePath().empty() ? handle->GetTargetFilePath() : handle->GetTempFilePath();
1677+
if (fileToValidate.empty()) {
1678+
return Aws::NoResult();
16471679
}
16481680

16491681
std::shared_ptr<Aws::Utils::Crypto::Hash> hashCalculator;
@@ -1660,13 +1692,16 @@ namespace Aws
16601692
}
16611693

16621694
auto fileStream = Aws::MakeShared<Aws::FStream>("TransferManager",
1663-
handle->GetTargetFilePath().c_str(),
1695+
fileToValidate.c_str(),
16641696
std::ios_base::in | std::ios_base::binary);
16651697

16661698
if (!fileStream->good()) {
16671699
AWS_LOGSTREAM_ERROR(CLASS_TAG, "Transfer handle [" << handle->GetId()
1668-
<< "] Failed to open downloaded file for checksum validation: " << handle->GetTargetFilePath());
1669-
return false;
1700+
<< "] Failed to open downloaded file for checksum validation: " << fileToValidate);
1701+
if (!handle->GetTempFilePath().empty()) {
1702+
Aws::FileSystem::RemoveFileIfExists(handle->GetTempFilePath().c_str());
1703+
}
1704+
return Aws::Client::AWSError<Aws::S3::S3Errors>(Aws::S3::S3Errors::INTERNAL_FAILURE, "FileOpenError", "Failed to open downloaded file for checksum validation", false);
16701705
}
16711706

16721707
unsigned char buffer[8192];
@@ -1678,18 +1713,33 @@ namespace Aws
16781713
if (!hashResult.IsSuccess()) {
16791714
AWS_LOGSTREAM_ERROR(CLASS_TAG, "Transfer handle [" << handle->GetId()
16801715
<< "] Failed to calculate checksum for downloaded file");
1681-
return false;
1716+
if (!handle->GetTempFilePath().empty()) {
1717+
Aws::FileSystem::RemoveFileIfExists(handle->GetTempFilePath().c_str());
1718+
}
1719+
return Aws::Client::AWSError<Aws::S3::S3Errors>(Aws::S3::S3Errors::INTERNAL_FAILURE, "ChecksumCalculationError", "Failed to calculate checksum for downloaded file", false);
16821720
}
16831721

16841722
Aws::String calculatedChecksum = Aws::Utils::HashingUtils::Base64Encode(hashResult.GetResult());
16851723
if (calculatedChecksum != handle->GetChecksum()) {
16861724
AWS_LOGSTREAM_ERROR(CLASS_TAG, "Transfer handle [" << handle->GetId()
16871725
<< "] Checksum validation failed. Expected: " << handle->GetChecksum()
16881726
<< ", Calculated: " << calculatedChecksum);
1689-
return false;
1727+
if (!handle->GetTempFilePath().empty()) {
1728+
Aws::FileSystem::RemoveFileIfExists(handle->GetTempFilePath().c_str());
1729+
}
1730+
return Aws::Client::AWSError<Aws::S3::S3Errors>(Aws::S3::S3Errors::INTERNAL_FAILURE, "ChecksumMismatch", "Downloaded file checksum does not match expected checksum", false);
16901731
}
16911732

1692-
return true;
1733+
// Validation succeeded, rename temp to target
1734+
if (!handle->GetTempFilePath().empty() && !handle->GetTargetFilePath().empty()) {
1735+
if (!Aws::FileSystem::RelocateFileOrDirectory(handle->GetTempFilePath().c_str(), handle->GetTargetFilePath().c_str())) {
1736+
AWS_LOGSTREAM_ERROR(CLASS_TAG, "Failed to rename temp file from " << handle->GetTempFilePath() << " to " << handle->GetTargetFilePath());
1737+
Aws::FileSystem::RemoveFileIfExists(handle->GetTempFilePath().c_str());
1738+
return Aws::Client::AWSError<Aws::S3::S3Errors>(Aws::S3::S3Errors::INTERNAL_FAILURE, "FileRenameError", "Failed to rename temporary file to target", false);
1739+
}
1740+
}
1741+
1742+
return Aws::NoResult();
16931743
}
16941744
}
16951745
}

0 commit comments

Comments
 (0)