Wasabi S3 Configuration¶
Bucket Details¶
| Property | Value |
|---|---|
| Provider | Wasabi Cloud Storage |
| Region | ap-southeast-1 (Asia Pacific — Southeast) |
| Endpoint | https://s3.ap-southeast-1.wasabisys.com |
| Bucket access | Private (no public access) |
| Versioning | Disabled |
| Object lifecycle | No expiry (permanent storage) |
boto3 Client Setup (wasabiboto3.py)¶
```python import boto3 from botocore.client import Config
s3 = boto3.client( 's3', endpoint_url='https://s3.ap-southeast-1.wasabisys.com', region_name='ap-southeast-1', aws_access_key_id=os.getenv('WASABI_ACCESS_KEY'), aws_secret_access_key=os.getenv('WASABI_SECRET_KEY'), config=Config( signature_version='s3v4', max_pool_connections=50, retries={'max_attempts': 3, 'mode': 'adaptive'}, connect_timeout=10, read_timeout=60 ) )
bucket_name = os.getenv('WASABI_BUCKET_NAME') ```
Key Configuration Parameters¶
| Parameter | Value | Reason |
|---|---|---|
signature_version |
s3v4 |
Required by Wasabi (SigV4) |
max_pool_connections |
50 | 50 concurrent S3 connections for parallel downloads |
connect_timeout |
10s | Fast failure if Wasabi unreachable |
read_timeout |
60s | Enough for encrypted chunk streaming (10 MB) |
max_attempts |
3 | Retry transient errors |
mode |
adaptive |
Auto back-off on throttling |
Object Namespace¶
All user files are stored under their email as prefix:
{user_email}/{folder_name}/{filename}
Examples:
studio@gmail.com/Wedding_2026/IMG_0001.CR2
studio@gmail.com/Wedding_2026/IMG_0001.CR2 ← encrypted version: [16B IV][ciphertext]
No two users share a prefix. Storage isolation is enforced at the prefix level.
Multipart Upload¶
All large file uploads use S3 Multipart Upload:
```python
Initiate¶
response = s3.create_multipart_upload(Bucket=bucket_name, Key=s3_key) upload_id = response['UploadId']
Upload parts (via presigned URL or direct upload)¶
s3.generate_presigned_url('upload_part', Params={...})
OR¶
s3.upload_part(Bucket=..., Key=..., UploadId=..., PartNumber=..., Body=...)
Complete¶
s3.complete_multipart_upload( Bucket=bucket_name, Key=s3_key, UploadId=upload_id, MultipartUpload={'Parts': etags_list} ) ```
Part size: 10 MB (enforced in frontend JS). Minimum part size (S3 requirement): 5 MB.
Presigned URLs¶
Used for browser-direct S3 access without exposing credentials:
```python
Generate presigned URL for browser to PUT a part directly¶
url = s3.generate_presigned_url( 'upload_part', Params={ 'Bucket': bucket_name, 'Key': s3_key, 'UploadId': upload_id, 'PartNumber': part_number }, ExpiresIn=3600 # 1 hour )
Generate presigned URL for browser to GET a file¶
url = s3.generate_presigned_url( 'get_object', Params={'Bucket': bucket_name, 'Key': s3_key}, ExpiresIn=86400 # 24 hours ) ```
S3 Object Metadata (Encrypted Files)¶
Encrypted files carry Wasabi S3 user metadata:
python
Metadata = {
'lenzeye-encrypted': 'true',
'lenzeye-key-version': key_version, # SHA-256 of user's active key
'lenzeye-hmac': hmac_hex, # 64-char hex HMAC-SHA256
}
This metadata is read on download to select the correct decryption key and verify integrity.
Wasabi vs AWS S3 Differences¶
| Feature | Wasabi | AWS S3 |
|---|---|---|
| Egress fees | None | Charged |
| API fees | None | Charged per request |
| S3 API compatibility | 100% | Reference |
| Eventual consistency window | Short | Immediate (since 2020) |
| Multipart abort | Manual | Manual |
Eventual consistency
After complete_multipart_upload, Wasabi may take 1–5 seconds before head_object returns the new metadata. The code retries head_object up to 5 times with 1-second sleep to handle this.
TL;DR¶
Wasabi AP Southeast-1, private bucket, SigV4 auth. 50-connection pool, adaptive retry, 60s read timeout. All files prefixed by {email}/. Encrypted files carry lenzeye-hmac and lenzeye-key-version in S3 metadata. Presigned URLs for all browser-direct access.