Files
ANSCORE/modules/ANSUtilities/ANSAWSS3.cpp

2704 lines
117 KiB
C++
Raw Normal View History

2026-03-28 16:54:11 +11:00
#include "ANSUtilities.h"
#include <iostream>
#include <CkFileAccess.h>
#include <CkAuthGoogle.h>
#include <CkSocket.h>
#include <CkGlobal.h>
#include <CkMailMan.h>
#include <CkEmail.h>
#include <CkMht.h>
#include <vector>
#include <CkBinData.h>
#include <CkStringBuilder.h>
#include <CkStream.h>
#include <fstream>
2026-04-17 07:03:03 +10:00
#include <chrono>
#include <thread>
#include <sstream>
#include <atomic>
2026-03-28 16:54:11 +11:00
static bool ansawss3LicenceValid = false;
static std::once_flag ansawss3LicenseOnceFlag;
namespace ANSCENTER
{
static void VerifyGlobalANSAWSS3License(const std::string& licenseKey) {
try {
static const std::vector<std::pair<int, std::string>> licenseChecks = {
{1000, "ANNHUB-LV"},
{1001, "DLHUB-LV"},
{1002, "ODHUB-LV"},
{1003, "ANSVIS"},
{1004, "ANSFR"},
{1005, "ANSOCR"},
{1006, "ANSALPR"},
{1007, "ANSCV"},
{1008, "ANSSRT"}
};
ansawss3LicenceValid = false;
for (const auto& [productId, productName] : licenseChecks) {
if (ANSCENTER::ANSLicenseHelper::LicenseVerification(licenseKey, productId, productName)) {
ansawss3LicenceValid = true;
break; // Stop at the first valid license
}
}
}
catch (const std::exception& e) {
ansawss3LicenceValid = false;
}
}
2026-04-17 07:03:03 +10:00
// Builds a per-call unique suffix for scratch file names used by
// multipart uploads. Combining thread id, instance address, and a
// monotonically-increasing counter guarantees no collision between
// parallel uploads — including two threads uploading the same source
// file at the same time (Fix 2).
static std::string MakeUniqueMultipartToken(const void* instancePtr) {
static std::atomic<uint64_t> counter{0};
std::ostringstream oss;
oss << std::hex
<< std::hash<std::thread::id>{}(std::this_thread::get_id()) << "_"
<< reinterpret_cast<uintptr_t>(instancePtr) << "_"
<< counter.fetch_add(1, std::memory_order_relaxed);
return oss.str();
}
// Private helper function to extract file name from a path
// Helper function to extract filename from path
2026-03-28 16:54:11 +11:00
std::string ANSAWSS3::ExtractFileName(const std::string& filePath) {
size_t pos = filePath.find_last_of("/\\");
if (pos != std::string::npos) {
return filePath.substr(pos + 1);
}
return filePath;
}
// Helper function to determine content type from file extension
std::string ANSAWSS3::GetContentType(const std::string& filePath) {
// Default to application/octet-stream for unknown types
std::string contentType = "application/octet-stream";
size_t extPos = filePath.find_last_of('.');
if (extPos == std::string::npos) {
return contentType;
}
// Extract and convert extension to lowercase
std::string fileExt = filePath.substr(extPos + 1);
std::transform(fileExt.begin(), fileExt.end(), fileExt.begin(), ::tolower);
// Image types
if (fileExt == "jpg" || fileExt == "jpeg") {
contentType = "image/jpeg";
}
else if (fileExt == "png") {
contentType = "image/png";
}
else if (fileExt == "gif") {
contentType = "image/gif";
}
else if (fileExt == "bmp") {
contentType = "image/bmp";
}
else if (fileExt == "webp") {
contentType = "image/webp";
}
else if (fileExt == "svg") {
contentType = "image/svg+xml";
}
else if (fileExt == "ico") {
contentType = "image/x-icon";
}
// Video types
else if (fileExt == "mp4") {
contentType = "video/mp4";
}
else if (fileExt == "avi") {
contentType = "video/x-msvideo";
}
else if (fileExt == "mov") {
contentType = "video/quicktime";
}
// Document types
else if (fileExt == "pdf") {
contentType = "application/pdf";
}
else if (fileExt == "json") {
contentType = "application/json";
}
else if (fileExt == "xml") {
contentType = "application/xml";
}
else if (fileExt == "txt") {
contentType = "text/plain";
}
return contentType;
}
ANSAWSS3::ANSAWSS3() {
_unlockCode = "ANSDRC.CB1122026_MEQCIFwO1IFQCG0BhZwsXFO68QUU6mDB5uge4duOsqOJanEyAiAB67ahqnXin4SRy0vIegISgbFlpldmbuS5gbU21GYVqA==";// "ANSDRC.CB1082025_Ax6P3M7F8B3d";//
_proxyHost = "";
_proxyPort = 0;
_proxyUsername = "";
_proxyPassword = "";
_bProxy = false;
_serviceName = "s3";
}
ANSAWSS3::~ANSAWSS3() {
StopRetry();
std::lock_guard<std::mutex> lk(_poolMutex);
_pool.clear(); // CkRest destructors handle disconnect
}
void ANSAWSS3::StopRetry() {
_stopRetry = true;
if (_retryThread.joinable()) {
_retryThread.join();
}
_retryInProgress = false;
}
// TryConnect — actual connection attempt (caller must hold _configMutex)
bool ANSAWSS3::TryConnect(bool& awsPath) {
auto testConn = CreateConnection();
if (!testConn) {
// Fallback: connect directly to baseDomain (MinIO, Ceph, etc.)
std::string savedFullURL = _fullAWSURL;
_fullAWSURL = _baseDomain;
testConn = CreateConnection();
if (!testConn) {
_fullAWSURL = savedFullURL; // restore on failure
return false;
}
awsPath = false;
_bAwsPath = false;
}
else {
awsPath = true;
_bAwsPath = true;
}
_bConnected = true;
// Return test connection to pool for reuse
{
std::lock_guard<std::mutex> lk(_poolMutex);
_pool.push_back(std::move(testConn));
}
return true;
}
void ANSAWSS3::RetryLoop() {
int attempt = 0;
while (!_stopRetry) {
// Wait 3 seconds (in 1-second increments so we can respond to stop quickly)
for (int i = 0; i < 3 && !_stopRetry; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
if (_stopRetry) break;
attempt++;
// Quick DNS check — is internet available?
{
CkSocket dnsCheck;
CkString dnsResult;
if (!dnsCheck.DnsLookup(_fullAWSURL.c_str(), 3000, dnsResult)) {
_logger.LogDebug("ANSAWSS3::RetryLoop",
"Retry #" + std::to_string(attempt) + " - no internet, retrying in 3s",
__FILE__, __LINE__);
continue;
}
}
// Internet is available — attempt real connection to validate auth/URL
{
std::lock_guard<std::mutex> lk(_configMutex);
bool awsPath = true;
if (TryConnect(awsPath)) {
_logger.LogDebug("ANSAWSS3::RetryLoop",
"Connected successfully after " + std::to_string(attempt) + " retries",
__FILE__, __LINE__);
_retryInProgress = false;
return;
}
else {
// Internet is available but connection failed (bad auth/URL)
// Stop retrying — no point, the parameters are wrong
_logger.LogError("ANSAWSS3::RetryLoop",
"Internet available but connection failed (check authentication or URL). Stopping retry.",
__FILE__, __LINE__);
_retryInProgress = false;
return;
}
}
}
_retryInProgress = false;
}
// ── Connection pool helpers ──
std::unique_ptr<S3Connection> ANSAWSS3::CreateConnection() {
auto conn = std::make_unique<S3Connection>();
2026-04-17 07:03:03 +10:00
// Apply request timeouts BEFORE Connect() so ConnectTimeoutMs takes
// effect on this very handshake. IdleTimeoutMs is sticky and applies
// to every subsequent request on this CkRest.
conn->rest.put_ConnectTimeoutMs(_connectTimeoutMs.load());
conn->rest.put_IdleTimeoutMs(_idleTimeoutMs.load());
2026-03-28 16:54:11 +11:00
// Connect
if (!conn->rest.Connect(_fullAWSURL.c_str(), _port, _bTls, _bAutoReconnect)) {
_logger.LogError("ANSAWSS3::CreateConnection", conn->rest.lastErrorText(), __FILE__, __LINE__);
return nullptr;
}
// Apply proxy if configured
if (_bProxy) {
conn->socket.put_HttpProxyHostname(_proxyHost.c_str());
conn->socket.put_HttpProxyPort(_proxyPort);
conn->socket.put_HttpProxyUsername(_proxyUsername.c_str());
conn->socket.put_HttpProxyPassword(_proxyPassword.c_str());
conn->socket.put_HttpProxyForHttp(_bProxy);
if (!conn->rest.UseConnection(conn->socket, true)) {
_logger.LogError("ANSAWSS3::CreateConnection - Proxy error", conn->rest.lastErrorText(), __FILE__, __LINE__);
return nullptr;
}
}
// Apply auth
if (_authReady) {
conn->authAws.put_AccessKey(_accessKey.c_str());
conn->authAws.put_SecretKey(_secretKey.c_str());
conn->authAws.put_ServiceName(_serviceName.c_str());
if (_bucketRegion != "us-east-1") {
if (!_bucketRegion.empty()) conn->authAws.put_Region(_bucketRegion.c_str());
}
if (!conn->rest.SetAuthAws(conn->authAws)) {
_logger.LogError("ANSAWSS3::CreateConnection - Auth error", conn->rest.lastErrorText(), __FILE__, __LINE__);
return nullptr;
}
}
return conn;
}
std::unique_ptr<S3Connection> ANSAWSS3::AcquireConnection() {
{
std::lock_guard<std::mutex> lk(_poolMutex);
if (!_pool.empty()) {
auto conn = std::move(_pool.back());
_pool.pop_back();
return conn;
}
}
// Pool empty — create a new connection (no lock held during network I/O)
std::lock_guard<std::mutex> cfgLk(_configMutex);
return CreateConnection();
}
void ANSAWSS3::ReleaseConnection(std::unique_ptr<S3Connection> conn) {
if (!conn) return;
std::lock_guard<std::mutex> lk(_poolMutex);
_pool.push_back(std::move(conn));
}
2026-04-17 07:03:03 +10:00
// Pre-populate the pool with `count` ready-to-use connections. Each
// connection performs a TLS handshake; doing `count` of them in parallel
// (rather than serially as AcquireConnection would on a cold pool) cuts
// the first-upload latency for workloads that fan out right after
// Connect() / SetAuthentication().
int ANSAWSS3::PrewarmConnectionPool(int count) {
if (count <= 0) return 0;
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
_logger.LogError("ANSAWSS3::PrewarmConnectionPool",
"Not ready: call Connect() and SetAuthentication() first",
__FILE__, __LINE__);
return 0;
}
// Hold _configMutex once across the whole prewarm. It blocks config
// writes for the duration, but does NOT block individual worker
// threads since CreateConnection() itself does not re-acquire it.
std::lock_guard<std::mutex> cfgLk(_configMutex);
std::vector<std::unique_ptr<S3Connection>> built(count);
std::vector<std::thread> workers;
workers.reserve(count);
for (int i = 0; i < count; ++i) {
workers.emplace_back([this, i, &built]() {
try {
built[i] = CreateConnection();
} catch (...) {
built[i] = nullptr;
}
});
}
for (auto& t : workers) t.join();
int added = 0;
{
std::lock_guard<std::mutex> lk(_poolMutex);
for (auto& conn : built) {
if (conn) {
_pool.push_back(std::move(conn));
++added;
}
}
}
_logger.LogDebug("ANSAWSS3::PrewarmConnectionPool",
"Added " + std::to_string(added) + "/" + std::to_string(count) + " connections to pool",
__FILE__, __LINE__);
return added;
}
// Update the per-connection request timeouts. New values are applied
// immediately to all pooled connections (IdleTimeoutMs is picked up on
// the next request; ConnectTimeoutMs is harmless on already-connected
// CkRest instances but takes effect if they ever reconnect) and baked
// into the defaults used by future CreateConnection() calls.
bool ANSAWSS3::SetTimeouts(int connectMs, int idleMs) {
if (connectMs < 1000 || idleMs < 1000) {
_logger.LogError("ANSAWSS3::SetTimeouts",
"Timeout values must be >= 1000 ms (got connect=" +
std::to_string(connectMs) + ", idle=" + std::to_string(idleMs) + ")",
__FILE__, __LINE__);
return false;
}
_connectTimeoutMs.store(connectMs);
_idleTimeoutMs.store(idleMs);
// Propagate to existing pooled connections so already-warm workers
// start using the new values on their next request.
{
std::lock_guard<std::mutex> lk(_poolMutex);
for (auto& conn : _pool) {
if (conn) {
conn->rest.put_ConnectTimeoutMs(connectMs);
conn->rest.put_IdleTimeoutMs(idleMs);
}
}
}
_logger.LogDebug("ANSAWSS3::SetTimeouts",
"connect=" + std::to_string(connectMs) + "ms, idle=" + std::to_string(idleMs) + "ms",
__FILE__, __LINE__);
return true;
}
// Drives a retry loop for a single upload operation. See the declaration
// comment in ANSUtilities.h for the AttemptResult contract.
//
// Retry policy (shared across all upload entry points):
// - kUploadMaxAttempts attempts total
// - kUploadRetryDelayMs between attempts (no delay after the last one)
// - std::exception thrown inside attemptFn is treated as Transient
// - Any other exception is treated as Permanent (bail immediately)
bool ANSAWSS3::UploadWithRetry(
const std::string& opName,
const std::function<AttemptResult(std::string& lastError)>& attemptFn) {
std::string lastError;
for (int attempt = 1; attempt <= kUploadMaxAttempts; ++attempt) {
AttemptResult result = AttemptResult::Transient;
try {
result = attemptFn(lastError);
}
catch (const std::exception& e) {
lastError = std::string("Exception: ") + e.what();
result = AttemptResult::Transient;
}
catch (...) {
lastError = "Unknown exception";
result = AttemptResult::Permanent;
}
if (result == AttemptResult::Success) {
return true;
}
if (result == AttemptResult::Permanent) {
_logger.LogError(opName,
"Permanent failure (no retry): " + lastError,
__FILE__, __LINE__);
return false;
}
// Transient — retry if attempts remain.
if (attempt < kUploadMaxAttempts) {
_logger.LogDebug(opName,
"Attempt " + std::to_string(attempt) + "/" +
std::to_string(kUploadMaxAttempts) + " failed: " + lastError +
" — retrying in " + std::to_string(kUploadRetryDelayMs) + "ms",
__FILE__, __LINE__);
std::this_thread::sleep_for(
std::chrono::milliseconds(kUploadRetryDelayMs));
}
}
_logger.LogError(opName,
"Upload failed after " + std::to_string(kUploadMaxAttempts) +
" attempts. Last error: " + lastError,
__FILE__, __LINE__);
return false;
}
2026-03-28 16:54:11 +11:00
void ANSAWSS3::CheckLicense() {
// Note: caller (Initialize) already holds _configMutex
try {
// Check once globally
std::call_once(ansawss3LicenseOnceFlag, [this]() {
VerifyGlobalANSAWSS3License(_licenseKey);
});
// Update this instance's local license flag
_isLicenseValid = ansawss3LicenceValid;
}
catch (const std::exception& e) {
this->_logger.LogFatal("ANSAWSS3::CheckLicense. Error:", e.what(), __FILE__, __LINE__);
}
}
void ANSAWSS3::CheckUnlockCode() {
// Note: caller (Initialize) already holds _configMutex
try {
CkGlobal glob;
_unlockCode = "ANSDRC.CB1122026_MEQCIFwO1IFQCG0BhZwsXFO68QUU6mDB5uge4duOsqOJanEyAiAB67ahqnXin4SRy0vIegISgbFlpldmbuS5gbU21GYVqA==";// "ANSDRC.CB1082025_Ax6P3M7F8B3d";
_isUnlockCodeValid = glob.UnlockBundle(_unlockCode.c_str());
if (!_isUnlockCodeValid) {
_logger.LogFatal("ANSAWSS3::CheckUnlockCode", glob.lastErrorText(), __FILE__, __LINE__);
return;
}
int status = glob.get_UnlockStatus();
if (status != 2) {
_logger.LogDebug("ANSAWSS3::CheckUnlockCode", "Unlocked in trial mode.", __FILE__, __LINE__);
}
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::CheckUnlockCode", e.what(), __FILE__, __LINE__);
}
}
bool ANSAWSS3::Initialize(const std::string& licenseKey) {
std::lock_guard<std::mutex> lock(_configMutex);
try {
_licenseKey = licenseKey;
CheckLicense();
CheckUnlockCode();
return _isLicenseValid && _isUnlockCodeValid;
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::Initialize", e.what(), __FILE__, __LINE__);
return false;
}
}
bool ANSAWSS3::SetServerProxy(const std::string& proxyHost, int proxyPort,const std::string& proxyUsername,const std::string& proxyPassword) {
// Early validation checks
if (!_isLicenseValid || !_isUnlockCodeValid) {
_logger.LogError("ANSAWSS3::SetServerProxy",
!_isLicenseValid ? "Invalid license" : "Invalid unlock code",
__FILE__, __LINE__);
return false;
}
std::lock_guard<std::mutex> lock(_configMutex);
try {
_proxyHost = proxyHost;
_proxyPort = proxyPort;
_proxyUsername = proxyUsername;
_proxyPassword = proxyPassword;
// Simplified proxy validation logic
_bProxy = !proxyHost.empty() && proxyPort > 0;
// Clear pool so new connections pick up proxy config
{
std::lock_guard<std::mutex> lk(_poolMutex);
_pool.clear();
}
return true;
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::SetServerProxy", e.what(), __FILE__, __LINE__);
return false;
}
}
// Returns: 1 = connected, 0 = failed (bad auth/URL), 2 = no internet (background retry started)
int ANSAWSS3::Connect(const std::string& baseDomain, const std::string& bucketRegion, const std::string& serviceName, int port, bool bTls, bool autoReconnect, bool &awsPath)
{
// Early validation checks
if (!_isLicenseValid || !_isUnlockCodeValid) {
_logger.LogError("ANSAWSS3::Connect",
!_isLicenseValid ? "Invalid license" : "Invalid unlock code",
__FILE__, __LINE__);
return 0;
}
// Stop any existing background retry
StopRetry();
std::lock_guard<std::mutex> lock(_configMutex);
try {
// Store parameters — strip http:// or https:// from baseDomain
{
std::string domain = baseDomain.empty() ? "amazonaws.com" : baseDomain;
if (domain.rfind("https://", 0) == 0) {
domain = domain.substr(8);
bTls = true; // caller said https, honour it
}
else if (domain.rfind("http://", 0) == 0) {
domain = domain.substr(7);
}
// Remove trailing slash if any
if (!domain.empty() && domain.back() == '/') domain.pop_back();
_baseDomain = domain;
}
_bucketRegion = bucketRegion.empty() ? "us-east-1" : bucketRegion;
_serviceName = serviceName.empty() ? "s3" : serviceName;
_fullAWSURL = _serviceName + "." + _bucketRegion + "." + _baseDomain;
_port = port;
_bTls = bTls;
_bAutoReconnect = autoReconnect;
// Clear pool so new connections use updated config
{
std::lock_guard<std::mutex> lk(_poolMutex);
_pool.clear();
}
// Check if internet is available (fast DNS check)
{
CkSocket dnsCheck;
CkString dnsResult;
if (!dnsCheck.DnsLookup(_fullAWSURL.c_str(), 3000, dnsResult)) {
// No internet — start background retry, return immediately
_logger.LogDebug("ANSAWSS3::Connect",
"No internet available, starting background retry for '" + _fullAWSURL + "'...",
__FILE__, __LINE__);
_stopRetry = false;
_retryInProgress = true;
_retryThread = std::thread(&ANSAWSS3::RetryLoop, this);
return 2; // no internet, retrying in background
}
}
// Internet is available — try real connection
if (TryConnect(awsPath)) {
return 1; // connected successfully
}
// Internet available but connection failed (bad auth, wrong URL, etc.)
return 0;
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::Connect", e.what(), __FILE__, __LINE__);
return 0;
}
}
bool ANSAWSS3::SetAuthentication(const std::string& accessKey,const std::string& secretKey) {
// Early validation checks
if (!_isLicenseValid || !_isUnlockCodeValid) {
_logger.LogError("ANSAWSS3::SetAuthentication",
!_isLicenseValid ? "Invalid license" : "Invalid unlock code",
__FILE__, __LINE__);
return false;
}
2026-04-17 07:03:03 +10:00
// Write credentials + clear the pool under _configMutex. The lock is
// released before the auto-prewarm below so PrewarmConnectionPool
// (which also takes _configMutex) doesn't deadlock.
{
std::lock_guard<std::mutex> lock(_configMutex);
try {
_accessKey = accessKey;
_secretKey = secretKey;
_authReady = true;
// Clear pool so new connections pick up new credentials.
2026-03-28 16:54:11 +11:00
std::lock_guard<std::mutex> lk(_poolMutex);
_pool.clear();
}
2026-04-17 07:03:03 +10:00
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::SetAuthentication", e.what(), __FILE__, __LINE__);
return false;
}
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
// Auto-prewarm the connection pool so the first N concurrent uploads
// don't serialize on TLS handshake. No-op if not connected yet.
static constexpr int kDefaultPrewarmCount = 8;
PrewarmConnectionPool(kDefaultPrewarmCount);
return true;
2026-03-28 16:54:11 +11:00
}
std::vector<std::string> ANSAWSS3::ListBuckets() {
// Early validation checks
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
_logger.LogError("ANSAWSS3::ListBuckets",
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return {};
}
try {
auto conn = AcquireConnection();
if (!conn) {
_logger.LogError("ListBuckets", "Failed to acquire S3 connection", __FILE__, __LINE__);
return {};
}
// Make request and capture response
conn->rest.put_Host(_fullAWSURL.c_str());
CkStringBuilder sbResponse;
if (!conn->rest.FullRequestNoBodySb("GET", "/", sbResponse)) {
_logger.LogError("ANSAWSS3::ListBuckets - Request failed",
conn->rest.lastErrorText(), __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return {};
}
// Check HTTP status code
int statusCode = conn->rest.get_ResponseStatusCode();
if (statusCode != 200) {
_logger.LogError("ANSAWSS3::ListBuckets - HTTP " + std::to_string(statusCode),
sbResponse.getAsString(), __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return {};
}
// Parse XML response
CkXml xml;
if (!xml.LoadSb(sbResponse, true)) {
_logger.LogError("ANSAWSS3::ListBuckets - XML parse error",
xml.lastErrorText(), __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return {};
}
// Extract bucket names
std::vector<std::string> bucketList;
int bucketCount = xml.NumChildrenHavingTag("Buckets|Bucket");
if (bucketCount > 0) {
bucketList.reserve(bucketCount);
for (int i = 0; i < bucketCount; ++i) {
xml.put_I(i);
const char* name = xml.getChildContent("Buckets|Bucket[i]|Name");
// Validate name before adding
if (name != nullptr && name[0] != '\0') {
bucketList.emplace_back(name);
}
}
}
ReleaseConnection(std::move(conn));
return bucketList;
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::ListBuckets", e.what(), __FILE__, __LINE__);
return {};
}
}
std::vector<std::string> ANSAWSS3::ListBucketObjects(const std::string& bucketName) {
// Early validation checks
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
_logger.LogError("ANSAWSS3::ListBucketObjects",
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return {};
}
if (bucketName.empty()) {
_logger.LogError("ANSAWSS3::ListBucketObjects",
"Bucket name is empty",
__FILE__, __LINE__);
return {};
}
std::vector<std::string> objectList;
try {
auto conn = AcquireConnection();
if (!conn) {
_logger.LogError("ListBucketObjects", "Failed to acquire S3 connection", __FILE__, __LINE__);
return {};
}
// Set bucket-specific endpoint
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
std::string marker = "";
bool isTruncated = true;
int pageCount = 0;
// Loop to handle pagination
while (isTruncated) {
pageCount++;
// Build the request path with marker if available
std::string basePath = _bAwsPath ? "/" : ("/" + bucketName + "/");
std::string requestPath = basePath;
if (!marker.empty()) {
requestPath = basePath + "?marker=" + marker;
}
CkStringBuilder sbResponse;
bool success = conn->rest.FullRequestNoBodySb("GET", requestPath.c_str(), sbResponse);
if (!success) {
_logger.LogError("ANSAWSS3::ListBucketObjects - Request failed on page " + std::to_string(pageCount),
conn->rest.lastErrorText(), __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return objectList; // Return what we have so far
}
int statusCode = conn->rest.get_ResponseStatusCode();
if (statusCode != 200) {
std::string errorMsg = "HTTP " + std::to_string(statusCode);
std::string response = sbResponse.getAsString();
if (!response.empty()) {
errorMsg += " - " + response;
}
_logger.LogError("ANSAWSS3::ListBucketObjects - Page " + std::to_string(pageCount),
errorMsg, __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return objectList; // Return what we have so far
}
// Parse XML response
CkXml xml;
bool loadSuccess = xml.LoadSb(sbResponse, true);
if (!loadSuccess) {
_logger.LogError("ANSAWSS3::ListBucketObjects",
"Failed to parse XML response on page " + std::to_string(pageCount) + ": " + std::string(xml.lastErrorText()),
__FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return objectList; // Return what we have so far
}
// Check if results are truncated (more pages to fetch)
const char* truncatedStr = xml.getChildContent("IsTruncated");
isTruncated = (truncatedStr != nullptr && std::string(truncatedStr) == "true");
// Get the marker for next page
if (isTruncated) {
// First try to get NextMarker
const char* nextMarker = xml.getChildContent("NextMarker");
if (nextMarker != nullptr && nextMarker[0] != '\0') {
marker = std::string(nextMarker);
}
else {
// If NextMarker is not present, use the last Key as marker
int count = xml.NumChildrenHavingTag("Contents");
if (count > 0) {
xml.put_I(count - 1); // Last element
const char* lastKey = xml.getChildContent("Contents[i]|Key");
if (lastKey != nullptr && lastKey[0] != '\0') {
marker = std::string(lastKey);
}
else {
isTruncated = false;
}
}
else {
isTruncated = false;
}
}
}
// Iterate through all Contents elements in this page
int count = xml.NumChildrenHavingTag("Contents");
for (int i = 0; i < count; i++) {
xml.put_I(i);
const char* key = xml.getChildContent("Contents[i]|Key");
if (key != nullptr && key[0] != '\0') {
objectList.emplace_back(key);
}
}
_logger.LogDebug("ANSAWSS3::ListBucketObjects",
"Page " + std::to_string(pageCount) + ": Retrieved " + std::to_string(count) +
" objects. Total so far: " + std::to_string(objectList.size()),
__FILE__, __LINE__);
}
_logger.LogDebug("ANSAWSS3::ListBucketObjects",
"Successfully listed " + std::to_string(objectList.size()) +
" objects in bucket: " + bucketName + " (" + std::to_string(pageCount) + " pages)",
__FILE__, __LINE__);
ReleaseConnection(std::move(conn));
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::ListBucketObjects",
std::string("Exception: ") + e.what(),
__FILE__, __LINE__);
objectList.clear(); // Ensure we return empty list on exception
}
return objectList;
}
std::vector<std::string> ANSAWSS3::ListBucketObjectsWithPrefix(const std::string& bucketName, const std::string& prefix) {
// Early validation checks
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
_logger.LogError("ANSAWSS3::ListBucketObjectsWithPrefix",
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return {};
}
if (bucketName.empty()) {
_logger.LogError("ANSAWSS3::ListBucketObjectsWithPrefix",
"Bucket name is empty",
__FILE__, __LINE__);
return {};
}
// prefix can be empty to list all objects
std::vector<std::string> objectList;
try {
auto conn = AcquireConnection();
if (!conn) {
_logger.LogError("ListBucketObjectsWithPrefix", "Failed to acquire S3 connection", __FILE__, __LINE__);
return {};
}
// Set bucket-specific endpoint
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
std::string marker = "";
bool isTruncated = true;
int pageCount = 0;
// Loop to handle pagination
while (isTruncated) {
pageCount++;
// Build the request path with prefix and marker
std::string basePath = _bAwsPath ? "/" : ("/" + bucketName + "/");
std::string requestPath = basePath;
bool hasParams = false;
// Add prefix parameter if provided
if (!prefix.empty()) {
requestPath += "?prefix=" + prefix;
hasParams = true;
}
// Add marker parameter if available
if (!marker.empty()) {
if (hasParams) {
requestPath += "&marker=" + marker;
}
else {
requestPath += "?marker=" + marker;
hasParams = true;
}
}
CkStringBuilder sbResponse;
bool success = conn->rest.FullRequestNoBodySb("GET", requestPath.c_str(), sbResponse);
if (!success) {
_logger.LogError("ANSAWSS3::ListBucketObjectsWithPrefix - Request failed on page " + std::to_string(pageCount),
conn->rest.lastErrorText(), __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return objectList; // Return what we have so far
}
int statusCode = conn->rest.get_ResponseStatusCode();
if (statusCode != 200) {
std::string errorMsg = "HTTP " + std::to_string(statusCode);
std::string response = sbResponse.getAsString();
if (!response.empty()) {
errorMsg += " - " + response;
}
_logger.LogError("ANSAWSS3::ListBucketObjectsWithPrefix - Page " + std::to_string(pageCount),
errorMsg, __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return objectList; // Return what we have so far
}
// Parse XML response
CkXml xml;
bool loadSuccess = xml.LoadSb(sbResponse, true);
if (!loadSuccess) {
_logger.LogError("ANSAWSS3::ListBucketObjectsWithPrefix",
"Failed to parse XML response on page " + std::to_string(pageCount) + ": " + std::string(xml.lastErrorText()),
__FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return objectList; // Return what we have so far
}
// Check if results are truncated (more pages to fetch)
const char* truncatedStr = xml.getChildContent("IsTruncated");
isTruncated = (truncatedStr != nullptr && std::string(truncatedStr) == "true");
// Get the marker for next page
if (isTruncated) {
// First try to get NextMarker
const char* nextMarker = xml.getChildContent("NextMarker");
if (nextMarker != nullptr && nextMarker[0] != '\0') {
marker = std::string(nextMarker);
}
else {
// If NextMarker is not present, use the last Key as marker
int count = xml.NumChildrenHavingTag("Contents");
if (count > 0) {
xml.put_I(count - 1); // Last element
const char* lastKey = xml.getChildContent("Contents[i]|Key");
if (lastKey != nullptr && lastKey[0] != '\0') {
marker = std::string(lastKey);
}
else {
isTruncated = false;
}
}
else {
isTruncated = false;
}
}
}
// Iterate through all Contents elements in this page
int count = xml.NumChildrenHavingTag("Contents");
for (int i = 0; i < count; i++) {
xml.put_I(i);
const char* key = xml.getChildContent("Contents[i]|Key");
if (key != nullptr && key[0] != '\0') {
objectList.emplace_back(key);
}
}
std::string prefixInfo = prefix.empty() ? "all objects" : "prefix '" + prefix + "'";
_logger.LogDebug("ANSAWSS3::ListBucketObjectsWithPrefix",
"Page " + std::to_string(pageCount) + ": Retrieved " + std::to_string(count) +
" objects for " + prefixInfo + ". Total so far: " + std::to_string(objectList.size()),
__FILE__, __LINE__);
}
std::string prefixInfo = prefix.empty() ? "" : " with prefix '" + prefix + "'";
_logger.LogDebug("ANSAWSS3::ListBucketObjectsWithPrefix",
"Successfully listed " + std::to_string(objectList.size()) +
" objects in bucket: " + bucketName + prefixInfo + " (" + std::to_string(pageCount) + " pages)",
__FILE__, __LINE__);
ReleaseConnection(std::move(conn));
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::ListBucketObjectsWithPrefix",
std::string("Exception: ") + e.what(),
__FILE__, __LINE__);
objectList.clear(); // Ensure we return empty list on exception
}
return objectList;
}
bool ANSAWSS3::CreateBucket(const std::string& bucketName) {
// Early validation checks
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
_logger.LogError("ANSAWSS3::CreateBucket",
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return false;
}
// Validate input parameters
if (bucketName.empty()) {
_logger.LogError("ANSAWSS3::CreateBucket",
"Bucket name cannot be empty",
__FILE__, __LINE__);
return false;
}
bool createSuccess = false;
try {
auto conn = AcquireConnection();
if (!conn) {
_logger.LogError("CreateBucket", "Failed to acquire S3 connection", __FILE__, __LINE__);
return false;
}
// Build global endpoint
std::string globalEndpoint = _serviceName + "." + _baseDomain;
CkStringBuilder sbBucketRegion;
sbBucketRegion.Append(_bucketRegion.c_str());
// We only need to specify the LocationConstraint if the bucket's region is NOT us-east-1
CkXml xml;
if (!sbBucketRegion.ContentsEqual("us-east-1", true)) {
xml.put_Tag("CreateBucketConfiguration");
xml.AddAttribute("xmlns", "http://s3.amazonaws.com/doc/2006-03-01/");
xml.UpdateChildContent("LocationConstraint", _bucketRegion.c_str());
}
// Set bucket-specific endpoint
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + globalEndpoint).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
// Make the call to create the bucket
std::string createPath = _bAwsPath ? "/" : ("/" + bucketName);
const char* responseStr = nullptr;
if (!sbBucketRegion.ContentsEqual("us-east-1", true)) {
responseStr = conn->rest.fullRequestString("PUT", createPath.c_str(), xml.getXml());
}
else {
// If the bucket is to be created in the us-east-1 region (the default region)
// just send a PUT with no body
responseStr = conn->rest.fullRequestNoBody("PUT", createPath.c_str());
}
if (conn->rest.get_LastMethodSuccess() != true) {
_logger.LogError("ANSAWSS3::CreateBucket - Request failed",
conn->rest.lastErrorText(), __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return false;
}
int statusCode = conn->rest.get_ResponseStatusCode();
if (statusCode == 200) {
_logger.LogDebug("ANSAWSS3::CreateBucket",
"Successfully created bucket: " + bucketName + " in region: " + _bucketRegion,
__FILE__, __LINE__);
createSuccess = true;
}
else {
std::string errorMsg = "HTTP " + std::to_string(statusCode);
if (responseStr != nullptr && responseStr[0] != '\0') {
CkXml responseXml;
if (responseXml.LoadXml(responseStr)) {
const char* errorCode = responseXml.getChildContent("Code");
const char* errorMessage = responseXml.getChildContent("Message");
if (errorCode != nullptr) {
errorMsg += " - " + std::string(errorCode);
}
if (errorMessage != nullptr) {
errorMsg += ": " + std::string(errorMessage);
}
}
else {
errorMsg += "\n" + std::string(responseStr);
}
}
if (statusCode == 409) {
_logger.LogError("ANSAWSS3::CreateBucket", errorMsg, __FILE__, __LINE__);
}
else {
_logger.LogError("ANSAWSS3::CreateBucket", errorMsg, __FILE__, __LINE__);
}
}
ReleaseConnection(std::move(conn));
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::CreateBucket",
std::string("Exception: ") + e.what(),
__FILE__, __LINE__);
}
return createSuccess;
}
bool ANSAWSS3::DeleteBucket(const std::string& bucketName) {
// Early validation checks
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
_logger.LogError("ANSAWSS3::DeleteBucket",
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return false;
}
if (bucketName.empty()) {
_logger.LogError("ANSAWSS3::DeleteBucket",
"Bucket name cannot be empty",
__FILE__, __LINE__);
return false;
}
bool deleteSuccess = false;
try {
auto conn = AcquireConnection();
if (!conn) {
_logger.LogError("DeleteBucket", "Failed to acquire S3 connection", __FILE__, __LINE__);
return false;
}
// Configure AWS authentication
conn->rest.ClearAuth();
conn->authAws.put_AccessKey(_accessKey.c_str());
conn->authAws.put_SecretKey(_secretKey.c_str());
conn->authAws.put_ServiceName(_serviceName.c_str());
bool authSuccess = conn->rest.SetAuthAws(conn->authAws);
if (!authSuccess) {
_logger.LogError("ANSAWSS3::DeleteBucket",
"Failed to set AWS authentication: " + std::string(conn->rest.lastErrorText()),
__FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return false;
}
// Set bucket-specific endpoint
std::string globalEndpoint = _serviceName + "." + _baseDomain;
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + globalEndpoint).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
std::string deletePath = _bAwsPath ? "/" : ("/" + bucketName);
CkStringBuilder sbResponse;
bool success = conn->rest.FullRequestNoBodySb("DELETE", deletePath.c_str(), sbResponse);
if (!success) {
_logger.LogError("ANSAWSS3::DeleteBucket",
"Failed to send DELETE request: " + std::string(conn->rest.lastErrorText()),
__FILE__, __LINE__);
}
else {
int statusCode = conn->rest.get_ResponseStatusCode();
// S3 returns 204 (No Content) for successful bucket deletion
if (statusCode == 204) {
_logger.LogDebug("ANSAWSS3::DeleteBucket",
"Successfully deleted bucket: " + bucketName,
__FILE__, __LINE__);
deleteSuccess = true;
}
else if (statusCode == 404) {
_logger.LogWarn("ANSAWSS3::DeleteBucket",
"Bucket not found (already deleted?): " + bucketName,
__FILE__, __LINE__);
}
else if (statusCode == 409) {
// Bucket not empty
std::string errorMsg = "HTTP 409 - Bucket is not empty and cannot be deleted";
std::string response = sbResponse.getAsString();
if (!response.empty()) {
CkXml responseXml;
if (responseXml.LoadXml(response.c_str())) {
const char* errorCode = responseXml.getChildContent("Code");
const char* errorMessage = responseXml.getChildContent("Message");
if (errorCode != nullptr) {
errorMsg += " - " + std::string(errorCode);
}
if (errorMessage != nullptr) {
errorMsg += ": " + std::string(errorMessage);
}
}
}
_logger.LogError("ANSAWSS3::DeleteBucket", errorMsg, __FILE__, __LINE__);
}
else {
std::string errorMsg = "HTTP " + std::to_string(statusCode);
std::string response = sbResponse.getAsString();
if (!response.empty()) {
errorMsg += " - " + response;
}
_logger.LogError("ANSAWSS3::DeleteBucket", errorMsg, __FILE__, __LINE__);
}
}
ReleaseConnection(std::move(conn));
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::DeleteBucket",
std::string("Exception: ") + e.what(),
__FILE__, __LINE__);
}
return deleteSuccess;
}
bool ANSAWSS3::CreateFolder(const std::string& bucketName, const std::string& prefix) {
// Early validation checks
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
_logger.LogError("ANSAWSS3::CreateFolder",
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return false;
}
if (bucketName.empty() || prefix.empty()) {
_logger.LogError("ANSAWSS3::CreateFolder",
"Bucket name or folder path is empty",
__FILE__, __LINE__);
return false;
}
bool createSuccess = false;
try {
auto conn = AcquireConnection();
if (!conn) {
_logger.LogError("CreateFolder", "Failed to acquire S3 connection", __FILE__, __LINE__);
return false;
}
// Ensure folder path ends with "/"
std::string normalizedPath = prefix;
if (normalizedPath.back() != '/') {
normalizedPath += '/';
}
// Set bucket-specific endpoint
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
// Object path is the folder name with trailing slash
std::string objectPath = _bAwsPath ? ("/" + normalizedPath) : ("/" + bucketName + "/" + normalizedPath);
// Create a zero-byte object to represent the folder
// Use PUT with empty body
const char* responseStr = conn->rest.fullRequestNoBody("PUT", objectPath.c_str());
if (conn->rest.get_LastMethodSuccess() != true) {
_logger.LogError("ANSAWSS3::CreateFolder - Request failed",
conn->rest.lastErrorText(), __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return false;
}
int statusCode = conn->rest.get_ResponseStatusCode();
if (statusCode == 200) {
_logger.LogDebug("ANSAWSS3::CreateFolder",
"Successfully created folder: " + normalizedPath + " in bucket: " + bucketName,
__FILE__, __LINE__);
createSuccess = true;
}
else {
std::string errorMsg = "HTTP " + std::to_string(statusCode);
if (responseStr != nullptr && responseStr[0] != '\0') {
CkXml responseXml;
if (responseXml.LoadXml(responseStr)) {
const char* errorCode = responseXml.getChildContent("Code");
const char* errorMessage = responseXml.getChildContent("Message");
if (errorCode != nullptr) {
errorMsg += " - " + std::string(errorCode);
}
if (errorMessage != nullptr) {
errorMsg += ": " + std::string(errorMessage);
}
}
else {
errorMsg += " - " + std::string(responseStr);
}
}
_logger.LogError("ANSAWSS3::CreateFolder", errorMsg, __FILE__, __LINE__);
}
ReleaseConnection(std::move(conn));
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::CreateFolder",
std::string("Exception: ") + e.what(),
__FILE__, __LINE__);
}
return createSuccess;
}
bool ANSAWSS3::DeleteFolder(const std::string& bucketName, const std::string& prefix) {
// Early validation checks
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
_logger.LogError("ANSAWSS3::DeleteFolder",
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return false;
}
if (bucketName.empty() || prefix.empty()) {
_logger.LogError("ANSAWSS3::DeleteFolder",
"Bucket name or folder path is empty",
__FILE__, __LINE__);
return false;
}
bool deleteSuccess = false;
try {
auto conn = AcquireConnection();
if (!conn) {
_logger.LogError("DeleteFolder", "Failed to acquire S3 connection", __FILE__, __LINE__);
return false;
}
// Ensure folder path ends with "/"
std::string normalizedPath = prefix;
if (normalizedPath.back() != '/') {
normalizedPath += '/';
}
// Set bucket-specific endpoint
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
std::string objectPath = _bAwsPath ? ("/" + normalizedPath) : ("/" + bucketName + "/" + normalizedPath);
CkStringBuilder sbResponse;
bool success = conn->rest.FullRequestNoBodySb("DELETE", objectPath.c_str(), sbResponse);
if (!success) {
_logger.LogError("ANSAWSS3::DeleteFolder",
"Failed to send DELETE request: " + std::string(conn->rest.lastErrorText()),
__FILE__, __LINE__);
}
else {
int statusCode = conn->rest.get_ResponseStatusCode();
// S3 returns 204 (No Content) for successful deletion
if (statusCode == 204 || statusCode == 200) {
_logger.LogDebug("ANSAWSS3::DeleteFolder",
"Successfully deleted folder marker: " + normalizedPath + " from bucket: " + bucketName,
__FILE__, __LINE__);
deleteSuccess = true;
}
else if (statusCode == 404) {
_logger.LogWarn("ANSAWSS3::DeleteFolder",
"Folder marker not found (already deleted?): " + normalizedPath,
__FILE__, __LINE__);
}
else {
std::string errorMsg = "HTTP " + std::to_string(statusCode);
std::string response = sbResponse.getAsString();
if (!response.empty()) {
errorMsg += " - " + response;
}
_logger.LogError("ANSAWSS3::DeleteFolder", errorMsg, __FILE__, __LINE__);
}
}
ReleaseConnection(std::move(conn));
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::DeleteFolder",
std::string("Exception: ") + e.what(),
__FILE__, __LINE__);
}
return deleteSuccess;
}
std::string ANSAWSS3::GetBucketRegion(const std::string& bucketName) {
// Early validation checks
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
_logger.LogError("ANSAWSS3::GetBucketRegion",
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return "";
}
if (bucketName.empty()) {
_logger.LogError("ANSAWSS3::GetBucketRegion",
"Bucket name is empty",
__FILE__, __LINE__);
return "";
}
std::string region = "";
try {
auto conn = AcquireConnection();
if (!conn) {
_logger.LogError("GetBucketRegion", "Failed to acquire S3 connection", __FILE__, __LINE__);
return "";
}
// Set bucket-specific endpoint
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
// Send the GET request to query the bucket location
std::string locationPath = _bAwsPath ? "/?location" : ("/" + bucketName + "?location");
const char* strResult = conn->rest.fullRequestNoBody("GET", locationPath.c_str());
if (conn->rest.get_LastMethodSuccess() != true) {
_logger.LogError("ANSAWSS3::GetBucketRegion - Request failed",
conn->rest.lastErrorText(), __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return "";
}
int responseStatusCode = conn->rest.get_ResponseStatusCode();
if (responseStatusCode != 200) {
std::string errorMsg = "HTTP " + std::to_string(responseStatusCode);
if (strResult != nullptr && strResult[0] != '\0') {
errorMsg += " - " + std::string(strResult);
}
_logger.LogError("ANSAWSS3::GetBucketRegion", errorMsg, __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return "";
}
// Parse XML response
CkXml xml;
bool success = xml.LoadXml(strResult);
if (!success) {
_logger.LogError("ANSAWSS3::GetBucketRegion - Failed to parse XML",
xml.lastErrorText(), __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return "";
}
// Get the region from XML content
const char* regionContent = xml.content();
if (regionContent != nullptr && regionContent[0] != '\0') {
region = std::string(regionContent);
// AWS returns empty string for us-east-1 (classic region)
// Some implementations may return "null" or empty
if (region.empty() || region == "null") {
region = "us-east-1";
_logger.LogDebug("ANSAWSS3::GetBucketRegion",
"Bucket '" + bucketName + "' is in default region: us-east-1",
__FILE__, __LINE__);
}
else {
_logger.LogDebug("ANSAWSS3::GetBucketRegion",
"Bucket '" + bucketName + "' is in region: " + region,
__FILE__, __LINE__);
}
}
else {
// Empty response typically means us-east-1
region = "us-east-1";
_logger.LogDebug("ANSAWSS3::GetBucketRegion",
"Empty response - assuming default region: us-east-1",
__FILE__, __LINE__);
}
ReleaseConnection(std::move(conn));
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::GetBucketRegion",
std::string("Exception: ") + e.what(),
__FILE__, __LINE__);
}
return region;
}
// Uploads text data from a file to the specified S3 bucket
bool ANSAWSS3::UploadTextData(const std::string& bucketName, const std::string& textFilePath, std::string& uploadedFilePath) {
2026-04-17 07:03:03 +10:00
const std::string kOp = "ANSAWSS3::UploadTextData";
// _logger.LogError(kOp,"Start Uploading...",__FILE__, __LINE__);
2026-04-17 07:03:03 +10:00
// Early validation (permanent — do NOT retry)
2026-03-28 16:54:11 +11:00
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp,
2026-03-28 16:54:11 +11:00
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return false;
}
if (bucketName.empty() || textFilePath.empty()) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp, "Bucket name or file path is empty", __FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
return false;
}
2026-04-17 07:03:03 +10:00
// Filename and content type are attempt-independent.
const std::string fileName = ExtractFileName(textFilePath);
if (fileName.empty()) {
_logger.LogError(kOp,
"Failed to extract filename from path: " + textFilePath,
__FILE__, __LINE__);
return false;
}
const std::string contentType = GetContentType(textFilePath);
const std::string objectPath =
_bAwsPath ? ("/" + fileName) : ("/" + bucketName + "/" + fileName);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
return UploadWithRetry(kOp,
[&](std::string& lastError) -> AttemptResult {
auto conn = AcquireConnection();
if (!conn) {
lastError = "Failed to acquire S3 connection";
return AttemptResult::Transient;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
// Read the file on each attempt (cheap; keeps the body fresh).
CkFileAccess fac;
const char* fileContents = fac.readEntireTextFile(textFilePath.c_str(), "utf-8");
if (!fac.get_LastMethodSuccess()) {
// File read failure is permanent (disk/IO, not network).
lastError = std::string("Failed to read file: ") + fac.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Permanent;
}
if (fileContents == nullptr || fileContents[0] == '\0') {
lastError = "File is empty: " + textFilePath;
ReleaseConnection(std::move(conn));
return AttemptResult::Permanent;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
conn->rest.AddHeader("Content-Type", contentType.c_str());
conn->rest.AddHeader("Content-Encoding", "gzip");
conn->rest.AddHeader("Expect", "100-continue");
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
const char* responseBodyStr =
conn->rest.fullRequestString("PUT", objectPath.c_str(), fileContents);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
if (!conn->rest.get_LastMethodSuccess()) {
lastError = conn->rest.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
int statusCode = conn->rest.get_ResponseStatusCode();
if (statusCode == 200) {
std::string scheme = _bTls ? "https://" : "http://";
uploadedFilePath = scheme + _fullAWSURL + "/" + bucketName + "/" + fileName;
_logger.LogDebug(kOp,
"Successfully uploaded: " + fileName + " | URL: " + uploadedFilePath,
__FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return AttemptResult::Success;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
lastError = "HTTP " + std::to_string(statusCode);
2026-03-28 16:54:11 +11:00
if (responseBodyStr != nullptr && responseBodyStr[0] != '\0') {
2026-04-17 07:03:03 +10:00
lastError += " - " + std::string(responseBodyStr);
2026-03-28 16:54:11 +11:00
}
ReleaseConnection(std::move(conn));
2026-04-17 07:03:03 +10:00
bool retryable = (statusCode >= 500 && statusCode <= 599)
|| statusCode == 408 || statusCode == 429;
return retryable ? AttemptResult::Transient : AttemptResult::Permanent;
});
2026-03-28 16:54:11 +11:00
}
bool ANSAWSS3::UploadBinaryData(const std::string& bucketName, const std::string& dataFilePath, std::string& uploadedFilePath) {
2026-04-17 07:03:03 +10:00
const std::string kOp = "ANSAWSS3::UploadBinaryData";
// _logger.LogError(kOp,"Start Uploading...",__FILE__, __LINE__);
2026-04-17 07:03:03 +10:00
// Early validation (permanent — do NOT retry)
2026-03-28 16:54:11 +11:00
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp,
2026-03-28 16:54:11 +11:00
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return false;
}
if (bucketName.empty() || dataFilePath.empty()) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp, "Bucket name or file path is empty", __FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
return false;
}
2026-04-17 07:03:03 +10:00
const std::string fileName = ExtractFileName(dataFilePath);
if (fileName.empty()) {
_logger.LogError(kOp,
"Failed to extract filename from path: " + dataFilePath,
__FILE__, __LINE__);
return false;
}
const std::string contentType = GetContentType(dataFilePath);
const std::string objectPath =
_bAwsPath ? ("/" + fileName) : ("/" + bucketName + "/" + fileName);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
return UploadWithRetry(kOp,
[&](std::string& lastError) -> AttemptResult {
auto conn = AcquireConnection();
if (!conn) {
lastError = "Failed to acquire S3 connection";
return AttemptResult::Transient;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
CkBinData binData;
if (!binData.LoadFile(dataFilePath.c_str())) {
lastError = std::string("Failed to load file: ") + binData.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Permanent; // disk/IO, not network
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
conn->rest.AddHeader("Content-Type", contentType.c_str());
conn->rest.AddHeader("Expect", "100-continue");
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
CkStringBuilder sbResponse;
if (!conn->rest.FullRequestBd("PUT", objectPath.c_str(), binData, sbResponse)) {
lastError = conn->rest.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
int statusCode = conn->rest.get_ResponseStatusCode();
if (statusCode == 200) {
std::string scheme = _bTls ? "https://" : "http://";
uploadedFilePath = scheme + _fullAWSURL + "/" + bucketName + "/" + fileName;
_logger.LogDebug(kOp,
"Successfully uploaded: " + fileName + " | URL: " + uploadedFilePath,
__FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return AttemptResult::Success;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
lastError = "HTTP " + std::to_string(statusCode);
std::string body = sbResponse.getAsString();
if (!body.empty()) lastError += " - " + body;
2026-03-28 16:54:11 +11:00
ReleaseConnection(std::move(conn));
2026-04-17 07:03:03 +10:00
bool retryable = (statusCode >= 500 && statusCode <= 599)
|| statusCode == 408 || statusCode == 429;
return retryable ? AttemptResult::Transient : AttemptResult::Permanent;
});
2026-03-28 16:54:11 +11:00
}
bool ANSAWSS3::UploadPrefixBinaryData(const std::string& bucketName, const std::string& prefix, const std::string& dataFilePath, const std::string& objectName, std::string& uploadedFilePath) {
2026-04-17 07:03:03 +10:00
const std::string kOp = "ANSAWSS3::UploadPrefixBinaryData";
//_logger.LogError(kOp,"Start Uploading...",__FILE__, __LINE__);
2026-04-17 07:03:03 +10:00
// Early validation (permanent — do NOT retry)
2026-03-28 16:54:11 +11:00
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp,
2026-03-28 16:54:11 +11:00
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return false;
}
if (bucketName.empty() || dataFilePath.empty()) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp, "Bucket name or file path is empty", __FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
return false;
}
2026-04-17 07:03:03 +10:00
// Resolve filename (from objectName, fall back to path).
std::string fileName = objectName;
if (fileName.empty()) {
fileName = ExtractFileName(dataFilePath);
if (fileName.empty()) {
_logger.LogError(kOp,
"Failed to extract filename from path: " + dataFilePath,
__FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
return false;
}
2026-04-17 07:03:03 +10:00
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
// Build once: the object key and the HTTP request path. Keeping
// them separate avoids the path-style bucket-duplication bug in
// the returned URL.
std::string objectKey; // "<prefix>/<fileName>" — no leading '/'
if (!prefix.empty()) {
std::string normalizedPrefix = prefix;
if (!normalizedPrefix.empty() && normalizedPrefix.front() == '/') {
normalizedPrefix = normalizedPrefix.substr(1);
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
if (!normalizedPrefix.empty() && normalizedPrefix.back() != '/') {
normalizedPrefix += '/';
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
objectKey = normalizedPrefix;
}
objectKey += fileName;
const std::string objectPath = _bAwsPath
? ("/" + objectKey)
: ("/" + bucketName + "/" + objectKey);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
const std::string contentType = GetContentType(dataFilePath);
return UploadWithRetry(kOp,
[&](std::string& lastError) -> AttemptResult {
auto conn = AcquireConnection();
if (!conn) {
lastError = "Failed to acquire S3 connection";
return AttemptResult::Transient;
}
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
CkBinData binData;
if (!binData.LoadFile(dataFilePath.c_str())) {
lastError = std::string("Failed to load file: ") + binData.lastErrorText();
2026-03-28 16:54:11 +11:00
ReleaseConnection(std::move(conn));
2026-04-17 07:03:03 +10:00
return AttemptResult::Permanent;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
conn->rest.AddHeader("Content-Type", contentType.c_str());
conn->rest.AddHeader("Expect", "100-continue");
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
CkStringBuilder sbResponse;
if (!conn->rest.FullRequestBd("PUT", objectPath.c_str(), binData, sbResponse)) {
lastError = conn->rest.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
int statusCode = conn->rest.get_ResponseStatusCode();
if (statusCode == 200) {
// Build canonical path-style URL from parts — independent
// of the addressing style we actually used.
std::string scheme = _bTls ? "https://" : "http://";
uploadedFilePath = scheme + _fullAWSURL + "/" + bucketName + "/" + objectKey;
_logger.LogDebug(kOp,
"Successfully uploaded to: " + objectKey + " | URL: " + uploadedFilePath,
__FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return AttemptResult::Success;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
lastError = "HTTP " + std::to_string(statusCode);
std::string body = sbResponse.getAsString();
if (!body.empty()) lastError += " - " + body;
ReleaseConnection(std::move(conn));
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
bool retryable = (statusCode >= 500 && statusCode <= 599)
|| statusCode == 408 || statusCode == 429;
return retryable ? AttemptResult::Transient : AttemptResult::Permanent;
});
}
bool ANSAWSS3::UploadFileStream(const std::string& bucketName, const std::string& dataFilePath, std::string& uploadedFilePath) {
const std::string kOp = "ANSAWSS3::UploadFileStream";
//_logger.LogError(kOp,"Start Uploading...",__FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
// Early validation (permanent — do NOT retry)
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
_logger.LogError(kOp,
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
2026-03-28 16:54:11 +11:00
__FILE__, __LINE__);
2026-04-17 07:03:03 +10:00
return false;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
if (bucketName.empty() || dataFilePath.empty()) {
_logger.LogError(kOp, "Bucket name or file path is empty", __FILE__, __LINE__);
return false;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
const std::string fileName = ExtractFileName(dataFilePath);
const std::string objectPath =
_bAwsPath ? ("/" + fileName) : ("/" + bucketName + "/" + fileName);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
return UploadWithRetry(kOp,
[&](std::string& lastError) -> AttemptResult {
auto conn = AcquireConnection();
if (!conn) {
lastError = "Failed to acquire S3 connection";
return AttemptResult::Transient;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
// Fresh stream per attempt (it holds OS file handle state).
CkStream fileStream;
fileStream.put_SourceFile(dataFilePath.c_str());
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
const char* responseStr =
conn->rest.fullRequestStream("PUT", objectPath.c_str(), fileStream);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
if (conn->rest.get_LastMethodSuccess() != true) {
lastError = conn->rest.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
int statusCode = conn->rest.get_ResponseStatusCode();
if (statusCode == 200) {
2026-03-28 16:54:11 +11:00
std::string scheme = _bTls ? "https://" : "http://";
2026-04-17 07:03:03 +10:00
uploadedFilePath = scheme + _fullAWSURL + "/" + bucketName + "/" + fileName;
_logger.LogDebug(kOp,
"Successfully uploaded file: " + fileName + " | URL: " + uploadedFilePath,
2026-03-28 16:54:11 +11:00
__FILE__, __LINE__);
2026-04-17 07:03:03 +10:00
ReleaseConnection(std::move(conn));
return AttemptResult::Success;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
lastError = "HTTP " + std::to_string(statusCode);
if (responseStr != nullptr && responseStr[0] != '\0') {
lastError += " - " + std::string(responseStr);
}
ReleaseConnection(std::move(conn));
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
bool retryable = (statusCode >= 500 && statusCode <= 599)
|| statusCode == 408 || statusCode == 429;
return retryable ? AttemptResult::Transient : AttemptResult::Permanent;
});
2026-03-28 16:54:11 +11:00
}
bool ANSAWSS3::UploadMultipartData(const std::string& bucketName,const std::string& dataFilePath, std::string& uploadedFilePath, int partSize) {
2026-04-17 07:03:03 +10:00
const std::string kOp = "ANSAWSS3::UploadMultipartData";
//_logger.LogError(kOp,"Start Uploading...",__FILE__, __LINE__);
2026-04-17 07:03:03 +10:00
// Early validation (permanent — do NOT retry)
2026-03-28 16:54:11 +11:00
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp,
2026-03-28 16:54:11 +11:00
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return false;
}
if (bucketName.empty() || dataFilePath.empty()) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp, "Bucket name or file path is empty", __FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
return false;
}
if (partSize < 5242880) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp,
2026-03-28 16:54:11 +11:00
"Part size must be at least 5MB (5242880 bytes)",
__FILE__, __LINE__);
return false;
}
2026-04-17 07:03:03 +10:00
{
2026-03-28 16:54:11 +11:00
CkFileAccess fac;
if (!fac.FileExists(dataFilePath.c_str())) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp, "File not found: " + dataFilePath, __FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
return false;
}
2026-04-17 07:03:03 +10:00
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
const std::string fileName = ExtractFileName(dataFilePath);
if (fileName.empty()) {
_logger.LogError(kOp,
"Failed to extract filename from path: " + dataFilePath,
2026-03-28 16:54:11 +11:00
__FILE__, __LINE__);
2026-04-17 07:03:03 +10:00
return false;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
const std::string objectPath =
_bAwsPath ? ("/" + fileName) : ("/" + bucketName + "/" + fileName);
const std::string fileFolderPath =
dataFilePath.substr(0, dataFilePath.find_last_of("/\\"));
// Fix 2: include a per-call unique token in the scratch-file name so
// two parallel multipart uploads of the same local file don't share
// the same partsList XML (which would race on create/delete).
const std::string partsListFilePath =
fileFolderPath + "/partsList_" + fileName + "_" +
MakeUniqueMultipartToken(this) + ".xml";
// Note on retry semantics for multipart:
// Each attempt performs a fresh Initiate → Parts → Complete cycle. We
// reset the on-disk partsList XML at the start of each attempt because
// ETags returned by a prior attempt are bound to that attempt's
// UploadId and are not valid for a new Initiate. Parts uploaded during
// a failed attempt therefore become orphans on S3 until the service's
// multipart-lifecycle rule cleans them up (default 7 days, configurable).
return UploadWithRetry(kOp,
[&](std::string& lastError) -> AttemptResult {
CkFileAccess fac;
// Ensure no stale parts list from a prior attempt.
if (fac.FileExists(partsListFilePath.c_str())) {
fac.FileDelete(partsListFilePath.c_str());
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
auto conn = AcquireConnection();
if (!conn) {
lastError = "Failed to acquire S3 connection";
return AttemptResult::Transient;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
// ---- STEP 1: INITIATE ----
conn->rest.ClearAllQueryParams();
conn->rest.AddQueryParam("uploads", "");
const char* responseXml = conn->rest.fullRequestNoBody("POST", objectPath.c_str());
if (!conn->rest.get_LastMethodSuccess()) {
lastError = std::string("Initiate failed: ") + conn->rest.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
}
int initStatus = conn->rest.get_ResponseStatusCode();
if (initStatus != 200) {
lastError = "Initiate HTTP " + std::to_string(initStatus);
if (responseXml != nullptr && responseXml[0] != '\0') lastError += " - " + std::string(responseXml);
ReleaseConnection(std::move(conn));
bool retryable = (initStatus >= 500 && initStatus <= 599) || initStatus == 408 || initStatus == 429;
return retryable ? AttemptResult::Transient : AttemptResult::Permanent;
}
CkXml xmlInit;
if (!xmlInit.LoadXml(responseXml)) {
lastError = std::string("Initiate XML parse error: ") + xmlInit.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
}
const char* uploadId = xmlInit.getChildContent("UploadId");
if (uploadId == nullptr || uploadId[0] == '\0') {
lastError = "UploadId not found in Initiate response";
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
_logger.LogDebug(kOp,
"Multipart initiated. UploadId: " + std::string(uploadId),
__FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
// ---- STEP 2: UPLOAD PARTS ----
fac.OpenForRead(dataFilePath.c_str());
int numParts = fac.GetNumBlocks(partSize);
fac.FileClose();
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
CkXml partsListXml;
partsListXml.put_Tag("CompleteMultipartUpload");
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
CkStringBuilder sbPartNumber;
for (int partNumber = 1; partNumber <= numParts; ++partNumber) {
sbPartNumber.Clear();
sbPartNumber.AppendInt(partNumber);
2026-03-28 16:54:11 +11:00
CkStream fileStream;
fileStream.put_SourceFile(dataFilePath.c_str());
fileStream.put_SourceFilePartSize(partSize);
2026-04-17 07:03:03 +10:00
fileStream.put_SourceFilePart(partNumber - 1);
2026-03-28 16:54:11 +11:00
conn->rest.ClearAllQueryParams();
conn->rest.AddQueryParam("partNumber", sbPartNumber.getAsString());
conn->rest.AddQueryParam("uploadId", uploadId);
const char* responseStr = conn->rest.fullRequestStream("PUT", objectPath.c_str(), fileStream);
if (!conn->rest.get_LastMethodSuccess()) {
2026-04-17 07:03:03 +10:00
lastError = "Part " + std::to_string(partNumber) + " failed: " + conn->rest.lastErrorText();
2026-03-28 16:54:11 +11:00
ReleaseConnection(std::move(conn));
2026-04-17 07:03:03 +10:00
return AttemptResult::Transient;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
int partStatus = conn->rest.get_ResponseStatusCode();
if (partStatus != 200) {
lastError = "Part " + std::to_string(partNumber) + " HTTP " + std::to_string(partStatus);
if (responseStr != nullptr && responseStr[0] != '\0') lastError += " - " + std::string(responseStr);
2026-03-28 16:54:11 +11:00
ReleaseConnection(std::move(conn));
2026-04-17 07:03:03 +10:00
bool retryable = (partStatus >= 500 && partStatus <= 599) || partStatus == 408 || partStatus == 429;
return retryable ? AttemptResult::Transient : AttemptResult::Permanent;
2026-03-28 16:54:11 +11:00
}
const char* etag = conn->rest.responseHdrByName("ETag");
if (!conn->rest.get_LastMethodSuccess() || etag == nullptr || etag[0] == '\0') {
2026-04-17 07:03:03 +10:00
lastError = "Part " + std::to_string(partNumber) + ": ETag missing from response";
2026-03-28 16:54:11 +11:00
ReleaseConnection(std::move(conn));
2026-04-17 07:03:03 +10:00
return AttemptResult::Transient;
2026-03-28 16:54:11 +11:00
}
CkXml* xPart = partsListXml.NewChild("Part", "");
xPart->NewChildInt2("PartNumber", partNumber);
xPart->NewChild2("ETag", etag);
delete xPart;
2026-04-17 07:03:03 +10:00
// Persist partsList for diagnostics; not used for resume in this attempt.
partsListXml.SaveXml(partsListFilePath.c_str());
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
// ---- STEP 3: COMPLETE ----
conn->rest.ClearAllQueryParams();
conn->rest.AddQueryParam("uploadId", uploadId);
responseXml = conn->rest.fullRequestString("POST", objectPath.c_str(), partsListXml.getXml());
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
if (!conn->rest.get_LastMethodSuccess()) {
lastError = std::string("Complete failed: ") + conn->rest.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
int completeStatus = conn->rest.get_ResponseStatusCode();
if (completeStatus != 200) {
lastError = "Complete HTTP " + std::to_string(completeStatus);
if (responseXml != nullptr && responseXml[0] != '\0') lastError += " - " + std::string(responseXml);
ReleaseConnection(std::move(conn));
bool retryable = (completeStatus >= 500 && completeStatus <= 599) || completeStatus == 408 || completeStatus == 429;
return retryable ? AttemptResult::Transient : AttemptResult::Permanent;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
// Cleanup parts list on success.
if (fac.FileExists(partsListFilePath.c_str())) {
fac.FileDelete(partsListFilePath.c_str());
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
std::string scheme = _bTls ? "https://" : "http://";
uploadedFilePath = scheme + _fullAWSURL + "/" + bucketName + "/" + fileName;
_logger.LogDebug(kOp,
"Multipart upload completed for: " + fileName + " | URL: " + uploadedFilePath,
__FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
ReleaseConnection(std::move(conn));
return AttemptResult::Success;
});
2026-03-28 16:54:11 +11:00
}
bool ANSAWSS3::UploadPrefixMultipartData(const std::string& bucketName, const std::string& prefix,const std::string& dataFilePath, const std::string& objectName, std::string& uploadedFilePath, int partSize) {
2026-04-17 07:03:03 +10:00
const std::string kOp = "ANSAWSS3::UploadPrefixMultipartData";
//_logger.LogError(kOp,"Start Uploading...",__FILE__, __LINE__);
2026-04-17 07:03:03 +10:00
// Early validation (permanent — do NOT retry)
2026-03-28 16:54:11 +11:00
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp,
2026-03-28 16:54:11 +11:00
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return false;
}
if (bucketName.empty() || dataFilePath.empty()) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp, "Bucket name or file path is empty", __FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
return false;
}
if (partSize < 5242880) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp,
2026-03-28 16:54:11 +11:00
"Part size must be at least 5MB (5242880 bytes)",
__FILE__, __LINE__);
return false;
}
2026-04-17 07:03:03 +10:00
{
2026-03-28 16:54:11 +11:00
CkFileAccess fac;
if (!fac.FileExists(dataFilePath.c_str())) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp, "File not found: " + dataFilePath, __FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
return false;
}
2026-04-17 07:03:03 +10:00
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
std::string fileName = objectName;
if (fileName.empty()) {
fileName = ExtractFileName(dataFilePath);
if (fileName.empty()) {
_logger.LogError(kOp,
"Failed to extract filename from path: " + dataFilePath,
__FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
return false;
}
2026-04-17 07:03:03 +10:00
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
// Build once: the object key and the HTTP request path. Keeping
// them separate avoids the path-style bucket-duplication bug in
// the returned URL.
std::string objectKey; // "<prefix>/<fileName>" — no leading '/'
if (!prefix.empty()) {
std::string normalizedPrefix = prefix;
if (!normalizedPrefix.empty() && normalizedPrefix.front() == '/') {
normalizedPrefix = normalizedPrefix.substr(1);
}
if (!normalizedPrefix.empty() && normalizedPrefix.back() != '/') {
normalizedPrefix += '/';
}
objectKey = normalizedPrefix;
}
objectKey += fileName;
const std::string objectPath = _bAwsPath
? ("/" + objectKey)
: ("/" + bucketName + "/" + objectKey);
// Parts-list file uses a sanitized filename for safe local storage.
// Fix 2: also append a per-call unique token so parallel multipart
// uploads of the same local file don't share the same scratch file.
const std::string fileFolderPath =
dataFilePath.substr(0, dataFilePath.find_last_of("/\\"));
std::string safeFileName = fileName;
std::replace(safeFileName.begin(), safeFileName.end(), '/', '_');
std::replace(safeFileName.begin(), safeFileName.end(), '\\', '_');
const std::string partsListFilePath =
fileFolderPath + "/partsList_" + safeFileName + "_" +
MakeUniqueMultipartToken(this) + ".xml";
// See note in UploadMultipartData regarding multipart retry semantics.
return UploadWithRetry(kOp,
[&](std::string& lastError) -> AttemptResult {
CkFileAccess fac;
if (fac.FileExists(partsListFilePath.c_str())) {
fac.FileDelete(partsListFilePath.c_str());
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
auto conn = AcquireConnection();
if (!conn) {
lastError = "Failed to acquire S3 connection";
return AttemptResult::Transient;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
// ---- STEP 1: INITIATE ----
conn->rest.ClearAllQueryParams();
conn->rest.AddQueryParam("uploads", "");
const char* responseXml = conn->rest.fullRequestNoBody("POST", objectPath.c_str());
if (!conn->rest.get_LastMethodSuccess()) {
lastError = std::string("Initiate failed: ") + conn->rest.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
int initStatus = conn->rest.get_ResponseStatusCode();
if (initStatus != 200) {
lastError = "Initiate HTTP " + std::to_string(initStatus);
if (responseXml != nullptr && responseXml[0] != '\0') lastError += " - " + std::string(responseXml);
ReleaseConnection(std::move(conn));
bool retryable = (initStatus >= 500 && initStatus <= 599) || initStatus == 408 || initStatus == 429;
return retryable ? AttemptResult::Transient : AttemptResult::Permanent;
}
CkXml xmlInit;
if (!xmlInit.LoadXml(responseXml)) {
lastError = std::string("Initiate XML parse error: ") + xmlInit.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
}
const char* uploadId = xmlInit.getChildContent("UploadId");
if (uploadId == nullptr || uploadId[0] == '\0') {
lastError = "UploadId not found in Initiate response";
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
_logger.LogDebug(kOp,
"Multipart initiated. UploadId: " + std::string(uploadId),
__FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
// ---- STEP 2: UPLOAD PARTS ----
fac.OpenForRead(dataFilePath.c_str());
int numParts = fac.GetNumBlocks(partSize);
fac.FileClose();
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
CkXml partsListXml;
partsListXml.put_Tag("CompleteMultipartUpload");
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
CkStringBuilder sbPartNumber;
for (int partNumber = 1; partNumber <= numParts; ++partNumber) {
sbPartNumber.Clear();
sbPartNumber.AppendInt(partNumber);
2026-03-28 16:54:11 +11:00
CkStream fileStream;
fileStream.put_SourceFile(dataFilePath.c_str());
fileStream.put_SourceFilePartSize(partSize);
2026-04-17 07:03:03 +10:00
fileStream.put_SourceFilePart(partNumber - 1);
2026-03-28 16:54:11 +11:00
conn->rest.ClearAllQueryParams();
conn->rest.AddQueryParam("partNumber", sbPartNumber.getAsString());
conn->rest.AddQueryParam("uploadId", uploadId);
const char* responseStr = conn->rest.fullRequestStream("PUT", objectPath.c_str(), fileStream);
if (!conn->rest.get_LastMethodSuccess()) {
2026-04-17 07:03:03 +10:00
lastError = "Part " + std::to_string(partNumber) + " failed: " + conn->rest.lastErrorText();
2026-03-28 16:54:11 +11:00
ReleaseConnection(std::move(conn));
2026-04-17 07:03:03 +10:00
return AttemptResult::Transient;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
int partStatus = conn->rest.get_ResponseStatusCode();
if (partStatus != 200) {
lastError = "Part " + std::to_string(partNumber) + " HTTP " + std::to_string(partStatus);
if (responseStr != nullptr && responseStr[0] != '\0') lastError += " - " + std::string(responseStr);
2026-03-28 16:54:11 +11:00
ReleaseConnection(std::move(conn));
2026-04-17 07:03:03 +10:00
bool retryable = (partStatus >= 500 && partStatus <= 599) || partStatus == 408 || partStatus == 429;
return retryable ? AttemptResult::Transient : AttemptResult::Permanent;
2026-03-28 16:54:11 +11:00
}
const char* etag = conn->rest.responseHdrByName("ETag");
if (!conn->rest.get_LastMethodSuccess() || etag == nullptr || etag[0] == '\0') {
2026-04-17 07:03:03 +10:00
lastError = "Part " + std::to_string(partNumber) + ": ETag missing from response";
2026-03-28 16:54:11 +11:00
ReleaseConnection(std::move(conn));
2026-04-17 07:03:03 +10:00
return AttemptResult::Transient;
2026-03-28 16:54:11 +11:00
}
CkXml* xPart = partsListXml.NewChild("Part", "");
xPart->NewChildInt2("PartNumber", partNumber);
xPart->NewChild2("ETag", etag);
delete xPart;
2026-04-17 07:03:03 +10:00
partsListXml.SaveXml(partsListFilePath.c_str());
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
// ---- STEP 3: COMPLETE ----
conn->rest.ClearAllQueryParams();
conn->rest.AddQueryParam("uploadId", uploadId);
responseXml = conn->rest.fullRequestString("POST", objectPath.c_str(), partsListXml.getXml());
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
if (!conn->rest.get_LastMethodSuccess()) {
lastError = std::string("Complete failed: ") + conn->rest.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
int completeStatus = conn->rest.get_ResponseStatusCode();
if (completeStatus != 200) {
lastError = "Complete HTTP " + std::to_string(completeStatus);
if (responseXml != nullptr && responseXml[0] != '\0') lastError += " - " + std::string(responseXml);
ReleaseConnection(std::move(conn));
bool retryable = (completeStatus >= 500 && completeStatus <= 599) || completeStatus == 408 || completeStatus == 429;
return retryable ? AttemptResult::Transient : AttemptResult::Permanent;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
if (fac.FileExists(partsListFilePath.c_str())) {
fac.FileDelete(partsListFilePath.c_str());
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
// Build canonical path-style URL from parts — independent
// of the addressing style we actually used.
std::string scheme = _bTls ? "https://" : "http://";
uploadedFilePath = scheme + _fullAWSURL + "/" + bucketName + "/" + objectKey;
_logger.LogDebug(kOp,
"Multipart upload completed. S3 key: " + objectKey + " | URL: " + uploadedFilePath,
__FILE__, __LINE__);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
ReleaseConnection(std::move(conn));
return AttemptResult::Success;
});
2026-03-28 16:54:11 +11:00
}
// Upload jpeg data
bool ANSAWSS3::UploadJpegImage(const std::string& bucketName, unsigned char* jpeg_string, int32 bufferLength, const std::string& fileName, std::string& uploadedFilePath) {
2026-04-17 07:03:03 +10:00
const std::string kOp = "ANSAWSS3::UploadJpegImage";
// _logger.LogError(kOp,"Start Uploading...",__FILE__, __LINE__);
2026-04-17 07:03:03 +10:00
// Early validation checks (permanent — do NOT retry)
2026-03-28 16:54:11 +11:00
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp,
2026-03-28 16:54:11 +11:00
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return false;
}
if (bucketName.empty() || bufferLength <= 0) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp,
2026-03-28 16:54:11 +11:00
"Bucket name or buffer length is invalid",
__FILE__, __LINE__);
return false;
}
2026-04-17 07:03:03 +10:00
// Object path is attempt-independent.
const std::string objectPath =
_bAwsPath ? ("/" + fileName) : ("/" + bucketName + "/" + fileName);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
return UploadWithRetry(kOp,
[&](std::string& lastError) -> AttemptResult {
auto conn = AcquireConnection();
if (!conn) {
lastError = "Failed to acquire S3 connection";
return AttemptResult::Transient;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
CkBinData binData;
CkByteData jpegBytes;
jpegBytes.append2(jpeg_string, static_cast<unsigned long>(bufferLength));
binData.AppendBinary(jpegBytes);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
conn->rest.AddHeader("Content-Type", "image/jpeg");
conn->rest.AddHeader("Expect", "100-continue");
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
CkStringBuilder sbResponse;
if (!conn->rest.FullRequestBd("PUT", objectPath.c_str(), binData, sbResponse)) {
lastError = conn->rest.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
int statusCode = conn->rest.get_ResponseStatusCode();
if (statusCode == 200) {
std::string scheme = _bTls ? "https://" : "http://";
uploadedFilePath = scheme + _fullAWSURL + "/" + bucketName + "/" + fileName;
_logger.LogDebug(kOp,
"Successfully uploaded: " + fileName + " | URL: " + uploadedFilePath,
__FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return AttemptResult::Success;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
lastError = "HTTP " + std::to_string(statusCode);
std::string body = sbResponse.getAsString();
if (!body.empty()) lastError += " - " + body;
2026-03-28 16:54:11 +11:00
ReleaseConnection(std::move(conn));
2026-04-17 07:03:03 +10:00
bool retryable = (statusCode >= 500 && statusCode <= 599)
|| statusCode == 408 || statusCode == 429;
return retryable ? AttemptResult::Transient : AttemptResult::Permanent;
});
2026-03-28 16:54:11 +11:00
}
bool ANSAWSS3::UploadPrefixJpegImage(const std::string& bucketName, const std::string& prefix,unsigned char* jpeg_string,int32 bufferLength,const std::string& fileName, std::string& uploadedFilePath) {
2026-04-17 07:03:03 +10:00
const std::string kOp = "ANSAWSS3::UploadPrefixJpegImage";
//_logger.LogError(kOp,"Start Uploading...",__FILE__, __LINE__);
2026-04-17 07:03:03 +10:00
// Early validation checks (permanent failures — do NOT retry)
2026-03-28 16:54:11 +11:00
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp,
2026-03-28 16:54:11 +11:00
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return false;
}
if (bucketName.empty() || bufferLength <= 0 || fileName.empty()) {
2026-04-17 07:03:03 +10:00
_logger.LogError(kOp,
2026-03-28 16:54:11 +11:00
"Bucket name, buffer length, or filename is invalid",
__FILE__, __LINE__);
return false;
}
2026-04-17 07:03:03 +10:00
// Build once: the object key (path within the bucket) and the HTTP
// request path (which depends on addressing style). Keeping these
// separate avoids the path-style bucket-duplication bug in the
// returned URL.
std::string objectKey; // "<prefix>/<fileName>" — no leading '/'
if (!prefix.empty()) {
std::string normalizedPrefix = prefix;
if (!normalizedPrefix.empty() && normalizedPrefix.front() == '/') {
normalizedPrefix = normalizedPrefix.substr(1);
}
if (!normalizedPrefix.empty() && normalizedPrefix.back() != '/') {
normalizedPrefix += '/';
}
objectKey = normalizedPrefix;
}
objectKey += fileName;
const std::string objectPath = _bAwsPath
? ("/" + objectKey)
: ("/" + bucketName + "/" + objectKey);
return UploadWithRetry(kOp,
[&](std::string& lastError) -> AttemptResult {
auto conn = AcquireConnection();
if (!conn) {
lastError = "Failed to acquire S3 connection";
return AttemptResult::Transient;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
CkBinData binData;
CkByteData jpegBytes;
jpegBytes.append2(jpeg_string, static_cast<unsigned long>(bufferLength));
binData.AppendBinary(jpegBytes);
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
conn->rest.AddHeader("Content-Type", "image/jpeg");
conn->rest.AddHeader("Expect", "100-continue");
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
CkStringBuilder sbResponse;
if (!conn->rest.FullRequestBd("PUT", objectPath.c_str(), binData, sbResponse)) {
lastError = conn->rest.lastErrorText();
ReleaseConnection(std::move(conn));
return AttemptResult::Transient;
}
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
int statusCode = conn->rest.get_ResponseStatusCode();
if (statusCode == 200) {
// Build the returned URL from parts (independent of
// addressing style) — always the canonical path-style URL.
std::string scheme = _bTls ? "https://" : "http://";
uploadedFilePath = scheme + _fullAWSURL + "/" + bucketName + "/" + objectKey;
_logger.LogError(kOp,
2026-04-17 07:03:03 +10:00
"Successfully uploaded: " + objectKey + " (" + std::to_string(bufferLength) + " bytes) | URL: " + uploadedFilePath,
__FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return AttemptResult::Success;
2026-03-28 16:54:11 +11:00
}
2026-04-17 07:03:03 +10:00
lastError = "HTTP " + std::to_string(statusCode);
std::string body = sbResponse.getAsString();
if (!body.empty()) lastError += " - " + body;
ReleaseConnection(std::move(conn));
2026-03-28 16:54:11 +11:00
2026-04-17 07:03:03 +10:00
bool retryable = (statusCode >= 500 && statusCode <= 599)
|| statusCode == 408 || statusCode == 429;
return retryable ? AttemptResult::Transient : AttemptResult::Permanent;
});
2026-03-28 16:54:11 +11:00
}
// Downloads
bool ANSAWSS3::DownloadFile(const std::string& bucketName,
const std::string& objectName,
const std::string& saveFilePath) {
// Early validation checks
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
_logger.LogError("ANSAWSS3::DownloadFile",
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return false;
}
if (bucketName.empty() || objectName.empty() || saveFilePath.empty()) {
_logger.LogError("ANSAWSS3::DownloadFile",
"Bucket name, object name, or save path is empty",
__FILE__, __LINE__);
return false;
}
bool downloadSuccess = false;
try {
auto conn = AcquireConnection();
if (!conn) {
_logger.LogError("DownloadFile", "Failed to acquire S3 connection", __FILE__, __LINE__);
return false;
}
// Set bucket-specific endpoint
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
// Normalize object name (remove leading slash if present)
std::string normalizedObjectName = objectName;
if (!normalizedObjectName.empty() && normalizedObjectName[0] == '/') {
normalizedObjectName = normalizedObjectName.substr(1);
}
std::string objectPath = _bAwsPath ? ("/" + normalizedObjectName) : ("/" + bucketName + "/" + normalizedObjectName);
_logger.LogDebug("ANSAWSS3::DownloadFile",
"Downloading: " + normalizedObjectName + " from bucket: " + bucketName,
__FILE__, __LINE__);
// Send GET request
if (!conn->rest.SendReqNoBody("GET", objectPath.c_str())) {
_logger.LogError("ANSAWSS3::DownloadFile - Request failed",
conn->rest.lastErrorText(), __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return false;
}
// Read the response header
int responseStatusCode = conn->rest.ReadResponseHeader();
if (responseStatusCode < 0) {
_logger.LogError("ANSAWSS3::DownloadFile - Failed to read response header",
conn->rest.lastErrorText(), __FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return false;
}
// We expect a 200 response status if the data is coming
if (responseStatusCode == 200) {
// Determine the final file path
std::string finalFilePath;
// Check if saveFilePath is a directory or a file
CkFileAccess fac;
bool isDirectory = false;
// If path exists and is a directory, or ends with path separator
if (fac.DirExists(saveFilePath.c_str())) {
isDirectory = true;
}
else if (!saveFilePath.empty() &&
(saveFilePath.back() == '\\' || saveFilePath.back() == '/')) {
isDirectory = true;
}
if (isDirectory) {
// Save path is a directory - append object name
finalFilePath = saveFilePath;
// Ensure path ends with separator
if (!finalFilePath.empty() &&
finalFilePath.back() != '\\' &&
finalFilePath.back() != '/') {
finalFilePath += "\\";
}
// Extract just the filename from object name (in case it has path separators)
std::string filename = normalizedObjectName;
size_t lastSlash = filename.find_last_of("/\\");
if (lastSlash != std::string::npos) {
filename = filename.substr(lastSlash + 1);
}
finalFilePath += filename;
}
else {
// Save path is the complete file path
finalFilePath = saveFilePath;
}
// Ensure the directory exists
size_t lastSeparator = finalFilePath.find_last_of("/\\");
if (lastSeparator != std::string::npos) {
std::string directory = finalFilePath.substr(0, lastSeparator);
// Create directory if it doesn't exist
if (!fac.DirExists(directory.c_str())) {
if (!fac.DirCreate(directory.c_str())) {
_logger.LogError("ANSAWSS3::DownloadFile - Failed to create directory",
"Directory: " + directory + " - " + fac.lastErrorText(),
__FILE__, __LINE__);
ReleaseConnection(std::move(conn));
return false;
}
_logger.LogDebug("ANSAWSS3::DownloadFile",
"Created directory: " + directory,
__FILE__, __LINE__);
}
}
// Setup stream to write to file
CkStream bodyStream;
bodyStream.put_SinkFile(finalFilePath.c_str());
// Read the response body to the stream
if (!conn->rest.ReadRespBodyStream(bodyStream, true)) {
_logger.LogError("ANSAWSS3::DownloadFile - Failed to read response body",
conn->rest.lastErrorText(), __FILE__, __LINE__);
}
else {
_logger.LogDebug("ANSAWSS3::DownloadFile",
"Successfully downloaded: " + normalizedObjectName + " to: " + finalFilePath,
__FILE__, __LINE__);
downloadSuccess = true;
}
}
else {
// Handle non-200 response
const char* errResponse = conn->rest.readRespBodyString();
std::string errorMsg = "HTTP " + std::to_string(responseStatusCode);
if (!conn->rest.get_LastMethodSuccess()) {
errorMsg += " - Failed to read error response: " + std::string(conn->rest.lastErrorText());
}
else {
if (errResponse != nullptr && errResponse[0] != '\0') {
errorMsg += " - " + std::string(errResponse);
}
}
_logger.LogError("ANSAWSS3::DownloadFile", errorMsg, __FILE__, __LINE__);
}
ReleaseConnection(std::move(conn));
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::DownloadFile",
std::string("Exception: ") + e.what(),
__FILE__, __LINE__);
}
return downloadSuccess;
}
// Delete
bool ANSAWSS3::DeleteBucketObject(const std::string& bucketName, const std::string& objectName) {
// Early validation checks
if (!_isLicenseValid || !_isUnlockCodeValid || !_bConnected) {
_logger.LogError("ANSAWSS3::DeleteBucketObject",
!_isLicenseValid ? "Invalid license" : !_isUnlockCodeValid ? "Invalid unlock code" : _retryInProgress.load() ? "Not connected (waiting for internet, retrying in background)" : "Not connected (no internet or connection not established)",
__FILE__, __LINE__);
return false;
}
if (bucketName.empty() || objectName.empty()) {
_logger.LogError("ANSAWSS3::DeleteBucketObject",
"Bucket name or object name is empty",
__FILE__, __LINE__);
return false;
}
bool deleteSuccess = false;
try {
auto conn = AcquireConnection();
if (!conn) {
_logger.LogError("DeleteBucketObject", "Failed to acquire S3 connection", __FILE__, __LINE__);
return false;
}
// Set bucket-specific endpoint
if (_bAwsPath) {
conn->rest.put_Host((bucketName + "." + _fullAWSURL).c_str());
} else {
conn->rest.put_Host(_fullAWSURL.c_str());
}
std::string objectPath = _bAwsPath ? ("/" + objectName) : ("/" + bucketName + "/" + objectName);
CkStringBuilder sbResponse;
bool success = conn->rest.FullRequestNoBodySb("DELETE", objectPath.c_str(), sbResponse);
if (!success) {
_logger.LogError("ANSAWSS3::DeleteBucketObject",
"Failed to send DELETE request: " + std::string(conn->rest.lastErrorText()),
__FILE__, __LINE__);
}
else {
int statusCode = conn->rest.get_ResponseStatusCode();
// S3 returns 204 (No Content) for successful deletion
// Also accept 200 as success
if (statusCode == 204 || statusCode == 200) {
_logger.LogDebug("ANSAWSS3::DeleteBucketObject",
"Successfully deleted object: " + objectName + " from bucket: " + bucketName,
__FILE__, __LINE__);
deleteSuccess = true;
}
else if (statusCode == 404) {
// Object doesn't exist - you may want to treat this as success or failure
// depending on your use case
// Uncomment the next line if you want to treat "not found" as success:
deleteSuccess = true;
}
else {
std::string errorMsg = "HTTP " + std::to_string(statusCode);
std::string response = sbResponse.getAsString();
if (!response.empty()) {
errorMsg += " - " + response;
}
_logger.LogError("ANSAWSS3::DeleteBucketObject", errorMsg, __FILE__, __LINE__);
}
}
ReleaseConnection(std::move(conn));
}
catch (const std::exception& e) {
_logger.LogFatal("ANSAWSS3::DeleteBucketObject",
std::string("Exception: ") + e.what(),
__FILE__, __LINE__);
}
return deleteSuccess;
}
}