Applies to: S3-Compatible Object Storage (PureStore backend)
Authentication: AWS Signature Version 4 (SigV4)
Audience: Developers integrating via cURL, Python, or .NET
Table of Contents
- Global Configuration Reference
- Obtaining Credentials
- Authentication Overview
- cURL — Using Built-in SigV4 Signing
- Python — boto3 SDK
- .NET — AWSSDK.S3
- Operation Reference
- Troubleshooting
1. Global Configuration Reference
All integrations must use the following configuration regardless of client language.
| Parameter | Value |
|---|---|
| Storage Endpoint | https://s3.in-west3.purestore.io |
| Region | in-west3 |
| Service Name | s3 |
| Signing Protocol | AWS Signature Version 4 (AWS4-HMAC-SHA256) |
| Virtual-Host URL | https://<YOUR_BUCKET>.s3.in-west3.purestore.io/<YOUR_OBJECT_KEY> |
| Path-Style URL | https://s3.in-west3.purestore.io/<YOUR_BUCKET>/<YOUR_OBJECT_KEY> |
| Multipart Upload | Supported |
| Pre-Signed URLs | Supported (GET & PUT) |
| Max Object Size (single PUT) | ~5 GB |
| Max Object Size (multipart) | ~5 TB |
- The SDK switches to multipart upload automatically for files above 8 MB (Python) and 16 MB (.NET) by default, so for most workloads the single-PUT limit is not encountered in practice.
URL Style: Path-style is used by default in cURL commands and in the .NET configuration below (
ForcePathStyle = true). Python boto3 defaults to virtual-host-style.
2. Obtaining Credentials
Credentials are generated from the CloudPe Management Portal (CMP):
- Log in to the CMP Dashboard
- Navigate to Object Storage → Access Keys
- Click Generate New Key
- Copy your Access Key ID and Secret Access Key — the secret is shown only once
See Enabling Access to S3 Storage for a step-by-step guide.
3. Authentication Overview
CloudPe Object Storage uses AWS Signature Version 4 (SigV4). Every authenticated request must carry these headers:
| Header | Description |
|---|---|
Host | Target hostname (s3.in-west3.purestore.io or <YOUR_BUCKET>.s3.in-west3.purestore.io) |
x-amz-date | Request timestamp in ISO 8601 format: YYYYMMDDTHHMMSSZ |
x-amz-content-sha256 | SHA-256 hex digest of the request body (empty body = e3b0c44...) |
Authorization | Full AWS4-HMAC-SHA256 credential string |
Authorization header structure:
AWS4-HMAC-SHA256
Credential=<AccessKeyID>/<Date>/<Region>/s3/aws4_request,
SignedHeaders=host;x-amz-content-sha256;x-amz-date,
Signature=<hex-signature>
Signing pipeline (4 steps):
- Canonical Request — Standardize HTTP method, URI, query string, headers, and SHA-256 body hash
- String to Sign — Combine algorithm name, timestamp, credential scope, and hash of the canonical request
- Derive Signing Key — HMAC-SHA256 chain:
Secret → Date → Region → "s3" → "aws4_request" - Calculate Signature — Final HMAC-SHA256 of the string to sign using the derived key
SDKs (boto3, AWSSDK.S3) and modern cURL (--aws-sigv4) perform all four steps automatically.
4. cURL — Using Built-in SigV4 Signing
cURL v7.75.0 and later includes native AWS SigV4 support via the --aws-sigv4 flag. This eliminates the need to manually compute canonical strings, HMAC chains, or hex signatures.
Flag syntax:
--aws-sigv4 "aws:amz:<region>:<service>"
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"
Can cURL generate pre-signed URLs?
No. cURL is an HTTP client — it can only use a pre-signed URL that has already been generated. Generating a pre-signed URL requires cryptographic signing outside of an HTTP request. Use the Python one-liner or AWS CLI shown in the Pre-Signed URL section below to generate the URL, then pass it to cURL.
Setup — Export Credentials Once
Run these in your terminal session before any of the commands below:
export AWS_ACCESS_KEY_ID="YOUR_ACCESS_KEY_ID"
export AWS_SECRET_ACCESS_KEY="YOUR_SECRET_ACCESS_KEY"
Windows (PowerShell): Use
$env:AWS_ACCESS_KEY_ID = "YOUR_ACCESS_KEY_ID"instead.
Placeholder reference used in all commands below:
| Placeholder | Replace with |
|---|---|
YOUR_BUCKET | Your bucket name (e.g. my-app-bucket) |
YOUR_OBJECT_KEY | Object path inside the bucket (e.g. reports/jan.pdf) |
YOUR_LOCAL_FILE | Local file path to upload (e.g. ./jan.pdf) |
YOUR_DOWNLOAD_PATH | Local path to save downloaded file (e.g. ./downloaded.pdf) |
YOUR_PREFIX | Folder-style prefix to filter by (e.g. logs/) |
cURL Operations
Create Bucket
curl -X PUT "https://s3.in-west3.purestore.io/YOUR_BUCKET" \
--aws-sigv4 "aws:amz:in-west3:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"
A successful create returns HTTP 200 OK with an empty body.
List Buckets
curl -X GET "https://s3.in-west3.purestore.io/" \
--aws-sigv4 "aws:amz:in-west3:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"
List Objects in a Bucket (GET Bucket)
Uses the list-type=2 protocol (ListObjectsV2):
curl -X GET "https://s3.in-west3.purestore.io/YOUR_BUCKET?list-type=2" \
--aws-sigv4 "aws:amz:in-west3:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"
With prefix filter (folder-style):
curl -X GET "https://s3.in-west3.purestore.io/YOUR_BUCKET?list-type=2&prefix=YOUR_PREFIX" \
--aws-sigv4 "aws:amz:in-west3:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"
Upload File (PUT Object)
curl -X PUT "https://s3.in-west3.purestore.io/YOUR_BUCKET/YOUR_OBJECT_KEY" \
--aws-sigv4 "aws:amz:in-west3:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary "@YOUR_LOCAL_FILE"
Upload into a folder prefix:
curl -X PUT "https://s3.in-west3.purestore.io/YOUR_BUCKET/YOUR_PREFIX/YOUR_OBJECT_KEY" \
--aws-sigv4 "aws:amz:in-west3:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary "@YOUR_LOCAL_FILE"
Download File (GET Object)
curl -X GET "https://s3.in-west3.purestore.io/YOUR_BUCKET/YOUR_OBJECT_KEY" \
--aws-sigv4 "aws:amz:in-west3:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-o "YOUR_DOWNLOAD_PATH"
Delete Object
curl -X DELETE "https://s3.in-west3.purestore.io/YOUR_BUCKET/YOUR_OBJECT_KEY" \
--aws-sigv4 "aws:amz:in-west3:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"
A successful delete returns HTTP 204 No Content with an empty body.
Delete Bucket
The bucket must be empty before it can be deleted.
curl -X DELETE "https://s3.in-west3.purestore.io/YOUR_BUCKET" \
--aws-sigv4 "aws:amz:in-west3:s3" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"
Pre-Signed URL — Full Options Reference
Important: cURL is an HTTP client and cannot cryptographically sign a URL. Pre-signed URLs must be generated by a tool that has access to your secret key (Python, AWS CLI, or .NET SDK), then used by cURL. The recipient of a pre-signed URL needs no credentials at all.
How a Pre-Signed URL Works
A pre-signed URL embeds the SigV4 signature directly into the query string, so any HTTP client can call it without credentials. The server validates the embedded signature on arrival and rejects the request if it has expired or been tampered with.
Anatomy of a generated URL:
https://s3.in-west3.purestore.io/YOUR_BUCKET/YOUR_OBJECT_KEY
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=YOUR_ACCESS_KEY_ID%2FYYYYMMDD%2Fin-west3%2Fs3%2Faws4_request
&X-Amz-Date=YYYYMMDDTHHMMSSZ
&X-Amz-Expires=3600
&X-Amz-SignedHeaders=host
&X-Amz-Signature=<64-char-hex>
| Parameter | Description |
|---|---|
X-Amz-Algorithm | Always AWS4-HMAC-SHA256 |
X-Amz-Credential | Access key + credential scope (date/region/service) — URL-encoded |
X-Amz-Date | Timestamp at signing time in ISO 8601 format |
X-Amz-Expires | Validity window in seconds from X-Amz-Date |
X-Amz-SignedHeaders | Headers that were included in the signature (minimum: host) |
X-Amz-Signature | 64-character hex HMAC-SHA256 signature |
Step 1 — Generate a Pre-Signed GET URL (Download Link)
Option A — Python one-liner (terminal):
# Valid for 1 hour (3600 seconds)
python3 -c "
import boto3
from botocore.client import Config
s3 = boto3.client(
's3',
endpoint_url='https://s3.in-west3.purestore.io',
aws_access_key_id='YOUR_ACCESS_KEY_ID',
aws_secret_access_key='YOUR_SECRET_ACCESS_KEY',
region_name='in-west3',
config=Config(signature_version='s3v4')
)
print(s3.generate_presigned_url(
'get_object',
Params={'Bucket': 'YOUR_BUCKET', 'Key': 'YOUR_OBJECT_KEY'},
ExpiresIn=3600
))
"
Option B — AWS CLI:
# Valid for 1 hour
aws s3 presign s3://YOUR_BUCKET/YOUR_OBJECT_KEY \
--endpoint-url https://s3.in-west3.purestore.io \
--region in-west3 \
--expires-in 3600
Expiry quick-reference:
| Use case | ExpiresIn value |
|---|---|
| One-time short share | 300 (5 minutes) |
| Standard API callback | 900 (15 minutes) |
| User-facing download link | 3600 (1 hour) |
| Overnight batch job | 86400 (24 hours) |
| Max practical window | 604800 (7 days) |
Step 2 — Generate a Pre-Signed PUT URL (Upload Link)
Use this to allow a client or third party to upload a file directly to your bucket without exposing your credentials.
Option A — Python one-liner (terminal):
python3 -c "
import boto3
from botocore.client import Config
s3 = boto3.client(
's3',
endpoint_url='https://s3.in-west3.purestore.io',
aws_access_key_id='YOUR_ACCESS_KEY_ID',
aws_secret_access_key='YOUR_SECRET_ACCESS_KEY',
region_name='in-west3',
config=Config(signature_version='s3v4')
)
print(s3.generate_presigned_url(
'put_object',
Params={
'Bucket': 'YOUR_BUCKET',
'Key': 'YOUR_OBJECT_KEY',
'ContentType': 'application/octet-stream'
},
ExpiresIn=3600
))
"
Option B — AWS CLI:
The AWS CLI
s3 presigncommand only generates GET URLs. Use the Python one-liner above or the .NET SDK to generate a PUT pre-signed URL.
Step 3 — Use the Pre-Signed URL with cURL
No credentials, no --aws-sigv4 flag — just the URL.
Download (GET):
curl -X GET "PASTE_PRESIGNED_GET_URL_HERE" \
-o "YOUR_DOWNLOAD_PATH"
With progress display:
curl -X GET "PASTE_PRESIGNED_GET_URL_HERE" \
-o "YOUR_DOWNLOAD_PATH" \
--progress-bar
Upload (PUT):
curl -X PUT "PASTE_PRESIGNED_PUT_URL_HERE" \
-H "Content-Type: application/octet-stream" \
--data-binary "@YOUR_LOCAL_FILE"
Content-Type must match exactly. If
ContentTypewas included when the PUT URL was generated (e.g.application/octet-stream), the upload request must send the sameContent-Typeheader. A mismatch causesHTTP 403 SignatureDoesNotMatch. If noContentTypewas set during generation, omit the-H "Content-Type: ..."header from cURL entirely.
With verbose output to confirm headers and response:
curl -v -X PUT "PASTE_PRESIGNED_PUT_URL_HERE" \
-H "Content-Type: application/octet-stream" \
--data-binary "@YOUR_LOCAL_FILE"
Quick Verification Test
To confirm a pre-signed GET URL is valid before distributing it:
# Should return HTTP 200 and the file content (or headers only with -I)
curl -I "PASTE_PRESIGNED_GET_URL_HERE"
A 200 OK with headers like Content-Length and ETag confirms the URL is valid and the object exists. A 403 means the URL has expired, the signature is invalid, or the object does not exist.
5. Python — boto3 SDK
Installation
pip install boto3
Client Setup
import boto3
from botocore.client import Config
# boto3 is the AWS-style S3 SDK for Python
# Creating an S3 client pointed at CloudPe's PureStore endpoint
s3 = boto3.client(
"s3",
endpoint_url="https://s3.in-west3.purestore.io",
aws_access_key_id="YOUR_ACCESS_KEY_ID",
aws_secret_access_key="YOUR_SECRET_ACCESS_KEY",
region_name="in-west3",
config=Config(signature_version="s3v4") # SigV4 is mandatory for this endpoint
)
Tip: To avoid hardcoding credentials, set environment variables instead:
export AWS_ACCESS_KEY_ID="YOUR_ACCESS_KEY_ID" export AWS_SECRET_ACCESS_KEY="YOUR_SECRET_ACCESS_KEY"Then omit
aws_access_key_idandaws_secret_access_keyfrom the client call — boto3 picks them up automatically.
Python Operations
Create Bucket
s3.create_bucket(
Bucket="YOUR_BUCKET",
CreateBucketConfiguration={"LocationConstraint": "in-west3"}
)
List Buckets
print("Buckets:")
response = s3.list_buckets()
for b in response["Buckets"]:
print(b["Name"])
List Objects in a Bucket
print("Bucket objects:")
response = s3.list_objects_v2(Bucket="YOUR_BUCKET")
for obj in response.get("Contents", []):
print(obj["Key"])
With prefix filter (folder-style):
response = s3.list_objects_v2(
Bucket="YOUR_BUCKET",
Prefix="YOUR_PREFIX" # e.g. "logs/" — behaves like a folder filter
)
for obj in response.get("Contents", []):
print(obj["Key"])
Upload File
High-level helper (recommended — handles multipart automatically for large files):
s3.upload_file(
"YOUR_LOCAL_FILE", # local file path
"YOUR_BUCKET", # bucket name
"YOUR_OBJECT_KEY" # destination key in bucket
)
Upload into a folder prefix:
s3.upload_file(
"YOUR_LOCAL_FILE",
"YOUR_BUCKET",
"YOUR_PREFIX/YOUR_OBJECT_KEY" # e.g. "logs/report.txt"
)
Raw PUT — stream a file handle directly:
with open("YOUR_LOCAL_FILE", "rb") as f:
s3.put_object(
Bucket="YOUR_BUCKET",
Key="YOUR_OBJECT_KEY",
Body=f
)
Explicit string or bytes body:
s3.put_object(
Bucket="YOUR_BUCKET",
Key="YOUR_OBJECT_KEY",
Body=b"Hello World"
)
Download File
High-level helper:
s3.download_file(
"YOUR_BUCKET", # bucket name
"YOUR_OBJECT_KEY", # object key
"YOUR_DOWNLOAD_PATH" # local destination path
)
Raw GET — read content into memory:
response = s3.get_object(
Bucket="YOUR_BUCKET",
Key="YOUR_OBJECT_KEY"
)
data = response["Body"].read()
print(data.decode())
Generate Pre-Signed URL
Pre-signed GET URL (download link — valid for 1 hour):
url = s3.generate_presigned_url(
"get_object",
Params={
"Bucket": "YOUR_BUCKET",
"Key": "YOUR_OBJECT_KEY"
},
ExpiresIn=3600 # seconds — 3600 = 1 hour
)
print("Presigned URL:", url)
Pre-signed PUT URL (upload link):
url = s3.generate_presigned_url(
"put_object",
Params={
"Bucket": "YOUR_BUCKET",
"Key": "YOUR_OBJECT_KEY"
},
ExpiresIn=3600
)
print("Upload link:", url)
The generated URL embeds the signature as query parameters. Anyone with the URL can download or upload within the expiry window — no credentials required.
Delete Object
s3.delete_object(
Bucket="YOUR_BUCKET",
Key="YOUR_OBJECT_KEY"
)
Delete Bucket
# Bucket must be empty before deletion
s3.delete_bucket(Bucket="YOUR_BUCKET")
6. .NET — AWSSDK.S3
Installation
Option A — NuGet CLI (from package source):
dotnet add package AWSSDK.S3 --source https://api.nuget.org/v3/index.json
Option B — If .NET was installed via the official installer from the website:
dotnet new nugetconfig
dotnet add package AWSSDK.S3
Or add directly to your .csproj:
<PackageReference Include="AWSSDK.S3" Version="3.*" />
<PackageReference Include="AWSSDK.Core" Version="3.*" />
Setting Credentials via Environment Variables
The AWSSDK automatically reads credentials from environment variables — no need to hardcode them in source code.
Command Prompt / Server (persists across sessions):
setx AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY_ID
setx AWS_SECRET_ACCESS_KEY=YOUR_SECRET_ACCESS_KEY
setx AWS_REGION=in-west3
setxwrites to the Windows registry. Restart your terminal after running these commands for the values to take effect.
PowerShell (current session only):
$env:AWS_ACCESS_KEY_ID = "YOUR_ACCESS_KEY_ID"
$env:AWS_SECRET_ACCESS_KEY = "YOUR_SECRET_ACCESS_KEY"
$env:AWS_REGION = "in-west3"
Full Program Example
The complete working program below uses credentials from environment variables and organises every operation as a named static method. Uncomment the calls in Main to activate each operation.
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Amazon.S3;
using Amazon.S3.Model;
class Program
{
private static AmazonS3Client? s3;
private static readonly HttpClient httpClient = new HttpClient();
static async Task Main(string[] args)
{
// Client config — reads AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
// from environment variables automatically (no hardcoded credentials)
var config = new AmazonS3Config
{
ServiceURL = "https://s3.in-west3.purestore.io",
AuthenticationRegion = "in-west3",
ForcePathStyle = true // path-style: s3.in-west3.purestore.io/bucket/key
};
s3 = new AmazonS3Client(config);
try
{
// ── CREATE BUCKET ──────────────────────────────────────────────
// await CreateBucketAsync("YOUR_BUCKET");
// ── LIST BUCKETS ───────────────────────────────────────────────
Console.WriteLine("Buckets");
await ListBucketsAsync();
// ── LIST OBJECTS IN BUCKET ─────────────────────────────────────
await ListObjectsAsync("YOUR_BUCKET");
// ── LIST OBJECTS WITH PREFIX (FOLDER FILTER) ───────────────────
// await ListObjectsWithPrefixAsync("YOUR_BUCKET", "YOUR_PREFIX/");
// ── UPLOAD FILE (PutObjectRequest with FilePath) ───────────────
// await UploadFileAsync("YOUR_LOCAL_FILE", "YOUR_BUCKET", "YOUR_OBJECT_KEY");
// ── RAW PUT via pre-signed URL + HttpClient ────────────────────
// Bypasses SDK signing engine; useful when direct PUT fails
await PutObjectRawAsync("YOUR_BUCKET", "YOUR_OBJECT_KEY", "YOUR_LOCAL_FILE");
// ── EXPLICIT STRING PUT ────────────────────────────────────────
// await PutObjectExplicitAsync("YOUR_BUCKET", "YOUR_OBJECT_KEY", "Hello World");
// ── DOWNLOAD FILE ──────────────────────────────────────────────
// await DownloadFileAsync("YOUR_BUCKET", "YOUR_OBJECT_KEY", "YOUR_DOWNLOAD_PATH");
// ── RAW GET (read content as string) ──────────────────────────
// await GetObjectExplicitAsync("YOUR_BUCKET", "YOUR_OBJECT_KEY");
// ── GENERATE PRE-SIGNED URLS ───────────────────────────────────
GeneratePresignedUrls("YOUR_BUCKET", "YOUR_OBJECT_KEY", "YOUR_UPLOAD_KEY");
// ── DELETE OBJECT ──────────────────────────────────────────────
// await DeleteObjectAsync("YOUR_BUCKET", "YOUR_OBJECT_KEY");
// ── DELETE BUCKET (must be empty first) ───────────────────────
// await DeleteBucketAsync("YOUR_BUCKET");
}
catch (AmazonS3Exception e)
{
Console.WriteLine($"S3 Error: {e.Message}");
}
catch (Exception e)
{
Console.WriteLine($"Critical Error: {e.Message}");
}
}
// =========================================================================
// OPERATIONS
// =========================================================================
static async Task CreateBucketAsync(string bucketName)
{
var request = new PutBucketRequest
{
BucketName = bucketName,
BucketRegionName = "in-west3"
};
await s3!.PutBucketAsync(request);
Console.WriteLine($"Bucket '{bucketName}' created successfully.");
}
static async Task ListBucketsAsync()
{
var response = await s3!.ListBucketsAsync();
foreach (var bucket in response.Buckets)
{
Console.WriteLine(bucket.BucketName);
}
}
static async Task ListObjectsAsync(string bucketName)
{
Console.WriteLine($"\n--- Bucket Objects ({bucketName}) ---");
var request = new ListObjectsV2Request { BucketName = bucketName };
var response = await s3!.ListObjectsV2Async(request);
foreach (var obj in response.S3Objects ?? new System.Collections.Generic.List<S3Object>())
{
Console.WriteLine(obj.Key);
}
}
static async Task ListObjectsWithPrefixAsync(string bucketName, string prefix)
{
var request = new ListObjectsV2Request
{
BucketName = bucketName,
Prefix = prefix // e.g. "logs/" — behaves like a folder filter
};
var response = await s3!.ListObjectsV2Async(request);
foreach (var obj in response.S3Objects ?? new System.Collections.Generic.List<S3Object>())
{
Console.WriteLine(obj.Key);
}
}
// High-level upload via PutObjectRequest with FilePath
static async Task UploadFileAsync(string filePath, string bucketName, string keyName)
{
var request = new PutObjectRequest
{
BucketName = bucketName,
Key = keyName,
FilePath = filePath
};
await s3!.PutObjectAsync(request);
Console.WriteLine($"Uploaded {filePath} to {bucketName}/{keyName}");
}
// Raw PUT: generates a short-lived pre-signed PUT URL, then streams the
// file over standard HttpClient — avoids SDK signing engine entirely.
// Useful if PutObjectAsync returns signature errors on this endpoint.
static async Task PutObjectRawAsync(string bucketName, string keyName, string localFilePath)
{
// A. Generate a pre-signed PUT URL (valid 15 minutes)
var putUrlRequest = new GetPreSignedUrlRequest
{
BucketName = bucketName,
Key = keyName,
Verb = HttpVerb.PUT,
Expires = DateTime.UtcNow.AddMinutes(15)
};
string uploadUrl = s3!.GetPreSignedURL(putUrlRequest);
// B. Stream the file over a plain HTTP PUT — no special headers needed
using var fileStream = new FileStream(localFilePath, FileMode.Open, FileAccess.Read);
using var content = new StreamContent(fileStream);
var response = await httpClient.PutAsync(uploadUrl, content);
if (response.IsSuccessStatusCode)
{
Console.WriteLine("Raw stream upload complete.");
}
else
{
string errorDetails = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Upload failed. Status: {response.StatusCode} | Body: {errorDetails}");
}
}
// PUT with an inline string body
static async Task PutObjectExplicitAsync(string bucketName, string keyName, string content)
{
var request = new PutObjectRequest
{
BucketName = bucketName,
Key = keyName,
ContentBody = content
};
await s3!.PutObjectAsync(request);
Console.WriteLine("Explicit text payload uploaded.");
}
// Download and write directly to a local file path
static async Task DownloadFileAsync(string bucketName, string keyName, string downloadPath)
{
var request = new GetObjectRequest { BucketName = bucketName, Key = keyName };
using var response = await s3!.GetObjectAsync(request);
await response.WriteResponseStreamToFileAsync(
downloadPath, false, System.Threading.CancellationToken.None);
Console.WriteLine($"Downloaded to: {downloadPath}");
}
// GET and read content as a string in memory
static async Task GetObjectExplicitAsync(string bucketName, string keyName)
{
var request = new GetObjectRequest { BucketName = bucketName, Key = keyName };
using var response = await s3!.GetObjectAsync(request);
using var reader = new StreamReader(response.ResponseStream);
string data = await reader.ReadToEndAsync();
Console.WriteLine($"Content: {data}");
}
// Generates both a GET (download) and PUT (upload) pre-signed URL
static void GeneratePresignedUrls(string bucketName, string getKey, string putKey)
{
// Pre-signed GET — anyone with this URL can download the object for 1 hour
var getRequest = new GetPreSignedUrlRequest
{
BucketName = bucketName,
Key = getKey,
Verb = HttpVerb.GET,
Expires = DateTime.UtcNow.AddHours(1)
};
string getUrl = s3!.GetPreSignedURL(getRequest);
Console.WriteLine($"Presigned URL for {getKey}: {getUrl}");
// Pre-signed PUT — anyone with this URL can upload to the key for 1 hour
var putRequest = new GetPreSignedUrlRequest
{
BucketName = bucketName,
Key = putKey,
Verb = HttpVerb.PUT,
Expires = DateTime.UtcNow.AddHours(1)
};
string putUrl = s3!.GetPreSignedURL(putRequest);
Console.WriteLine($"Upload link: {putUrl}");
}
static async Task DeleteObjectAsync(string bucketName, string keyName)
{
var request = new DeleteObjectRequest { BucketName = bucketName, Key = keyName };
await s3!.DeleteObjectAsync(request);
Console.WriteLine($"Deleted object: {keyName}");
}
static async Task DeleteBucketAsync(string bucketName)
{
// Bucket must be empty — delete all objects first
var request = new DeleteBucketRequest { BucketName = bucketName };
await s3!.DeleteBucketAsync(request);
Console.WriteLine($"Deleted bucket: {bucketName}");
}
}
7. Operation Reference
Quick side-by-side reference for all operations across all three clients.
| Operation | cURL | Python (boto3) | .NET (AWSSDK.S3) |
|---|---|---|---|
| Create Bucket | PUT /YOUR_BUCKET | s3.create_bucket(Bucket=...) | s3.PutBucketAsync(...) |
| List Buckets | GET / | s3.list_buckets() | s3.ListBucketsAsync() |
| List Objects | GET /YOUR_BUCKET?list-type=2 | s3.list_objects_v2(Bucket=...) | s3.ListObjectsV2Async(...) |
| List with Prefix | ?list-type=2&prefix=YOUR_PREFIX | list_objects_v2(Prefix=...) | ListObjectsV2Request{ Prefix=... } |
| Upload (high-level) | PUT /YOUR_BUCKET/YOUR_OBJECT_KEY --data-binary | s3.upload_file(local, bucket, key) | UploadFileAsync via PutObjectRequest{ FilePath } |
| Upload (raw / stream) | same as above | s3.put_object(Bucket, Key, Body) | PutObjectRawAsync via pre-signed URL + HttpClient |
| Upload (inline body) | --data "content" | put_object(Body=b"...") | PutObjectExplicitAsync via ContentBody |
| Download (to file) | GET /bucket/key -o path | s3.download_file(bucket, key, local) | DownloadFileAsync via WriteResponseStreamToFileAsync |
| Download (to memory) | same + capture output | s3.get_object(Bucket, Key) | GetObjectExplicitAsync via StreamReader |
| Delete Object | DELETE /YOUR_BUCKET/YOUR_OBJECT_KEY | s3.delete_object(Bucket, Key) | s3.DeleteObjectAsync(...) |
| Delete Bucket | DELETE /YOUR_BUCKET | s3.delete_bucket(Bucket) | s3.DeleteBucketAsync(...) |
| Generate Pre-signed GET | not possible — use Python/CLI to generate | generate_presigned_url("get_object") | GetPreSignedURL(Verb=GET) |
| Generate Pre-signed PUT | not possible — use Python/CLI to generate | generate_presigned_url("put_object") | GetPreSignedURL(Verb=PUT) |
| Use Pre-signed URL | curl -X GET/PUT "PRESIGNED_URL" | use with requests.get/put(url) | use with HttpClient.GetAsync/PutAsync(url) |
8. Troubleshooting
| Error | Likely Cause | Fix |
|---|---|---|
SignatureDoesNotMatch | Wrong secret key, or pre-signed PUT Content-Type mismatch | Verify credentials; ensure Content-Type on upload matches what was signed |
InvalidAccessKeyId | Access key is wrong or has been rotated | Re-generate keys in CMP → Object Storage → Access Keys |
RequestTimeTooSkewed | System clock is > 15 minutes off from server time | Linux: sudo ntpdate pool.ntp.org; Windows: sync via Date & Time settings |
NoSuchBucket | Bucket does not exist or name is misspelled | Verify bucket name via List Buckets; confirm region is in-west3 |
NoSuchKey | Object key path does not exist in the bucket | List objects first to confirm the exact key |
EntityTooLarge | Single-PUT object exceeds the endpoint’s size limit | Use upload_file (Python) or PutObjectRawAsync via multipart — both split large files automatically |
| SigV2 / auth error | Client is sending deprecated Signature Version 2 | Set signature_version="s3v4" in boto3; ensure AuthenticationRegion = "in-west3" is explicitly defined in .NET config (SigV4 is handled automatically). |
| .NET payload error | Blank S3 Error: or dropped connection on upload | The .NET SDK’s high-level payload formatting is conflicting with PureStore. Use the provided PutObjectRawAsync method to stream via a standard HTTP client instead. |
cURL: --aws-sigv4 unknown option | cURL version is older than v7.75.0 | Upgrade cURL: apt install curl (Linux) / brew install curl (macOS) / curl.se (Windows) |
.NET: SSL/TLS error | Outdated .NET runtime or missing CA certificate | Update .NET SDK; verify system CA certificate store |
| 403 on pre-signed PUT | Recipient used wrong Content-Type on upload | The upload Content-Type header must exactly match the value used when generating the URL |
setx values not recognised | Terminal not restarted after setx | Close and reopen Command Prompt / PowerShell after running setx |