Steps
-
Create a bucket and an access key
Do it in the console of your storage service. Give the key access to this one bucket only: a leaked key then reaches one bucket, not the account.
-
Put the key in the environment
Both SDKs read these two variables, so the key stays out of your source files. Boto3 also reads
~/.aws/credentials.$ export AWS_ACCESS_KEY_ID=your-key-id $ export AWS_SECRET_ACCESS_KEY=your-secret
-
Upload from Python
Install boto3 in a virtual environment.
upload_filetakes a path;upload_fileobjtakes a file opened in binary mode;put_objectsends a small body in one request.$ python3 -m venv .venv && source .venv/bin/activate $ pip install boto3
import boto3 s3 = boto3.client("s3", region_name="eu-west-1") # a file from disk s3.upload_file("report.pdf", "my-bucket", "reports/report.pdf") # a file object, opened in binary mode with open("report.pdf", "rb") as f: s3.upload_fileobj(f, "my-bucket", "reports/copy.pdf") # a small body in one request s3.put_object( Bucket="my-bucket", Key="notes/hello.txt", Body=b"hello", ContentType="text/plain", ) -
Upload from JavaScript
The AWS SDK for JavaScript v3 sends one command per call. Use an ES module (
.mjs) soimportand top-levelawaitwork.$ npm install @aws-sdk/client-s3import { readFileSync } from "node:fs"; import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; const client = new S3Client({ region: "eu-west-1" }); await client.send( new PutObjectCommand({ Bucket: "my-bucket", Key: "reports/report.pdf", Body: readFileSync("report.pdf"), ContentType: "application/pdf", }), ); -
Send big files in parts
In Python,
upload_filesplits a large file into chunks and uploads them in parallel (a multipart upload). In JavaScript, a singlePutObjectCommandsends everything at once. For a big file or a stream, useUploadfrom@aws-sdk/lib-storage. It does multipart under the hood: parts of at least 5 MB, 4 parts at a time by default, and it aborts a failed upload unless leavePartsOnError is true.$ npm install @aws-sdk/lib-storageimport { createReadStream } from "node:fs"; import { S3Client } from "@aws-sdk/client-s3"; import { Upload } from "@aws-sdk/lib-storage"; const client = new S3Client({ region: "eu-west-1" }); const upload = new Upload({ client, params: { Bucket: "my-bucket", Key: "backups/big.bin", Body: createReadStream("big.bin"), }, }); upload.on("httpUploadProgress", (p) => console.log(p.loaded, "bytes")); await upload.done(); -
Point the client at another S3-compatible service
For storage that is not Amazon's, add the service's address. In Python,
endpoint_urlis the complete URL to use for the client, scheme included. In JavaScript, passendpoint, and addforcePathStyle: trueif the service wants path-style URLs.# Python s3 = boto3.client("s3", endpoint_url="https://s3.example.com", region_name="us-east-1") // JavaScript const client = new S3Client({ region: "us-east-1", endpoint: "https://s3.example.com", forcePathStyle: true, });
Check that it works
List the bucket and download one file back. A key you uploaded shows up with the size you expect. We ran the Python and JavaScript upload code on this page against a local S3 test server, including a 20 MB file in parts.
for obj in s3.list_objects_v2(Bucket="my-bucket")["Contents"]:
print(obj["Key"], obj["Size"])
s3.download_file("my-bucket", "reports/report.pdf", "back.pdf")To hand a file to someone who has no key, make a temporary link. It stops working after ExpiresIn seconds. In JavaScript, getSignedUrl from @aws-sdk/s3-request-presigner does the same: getSignedUrl(client, command, { expiresIn: 3600 }); the default is 900 seconds, the S3 maximum is 7 days.
url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": "my-bucket", "Key": "reports/report.pdf"},
ExpiresIn=3600,
)The short way: Uno cloud storage
Every Uno plan includes S3-compatible storage that stays when you delete a computer. Make an access key for a bucket and the boto3 and AWS SDK code above works as it is, with Uno's endpoint. Or skip keys and send files through presigned links. Both are below.
Small: $5 a month
Upload to Uno with an S3 key
Make a key for one bucket: on the Storage page in the console, or with POST /api/v1/buckets/{id}/keys and a permission of read_write, read_only or write_only. The answer carries access_key_id, secret_access_key, endpoint and bucket. The secret is shown once, so put it in your environment right away. One key opens exactly one bucket, and deleting the key cuts it off at once.
export AWS_ACCESS_KEY_ID=... # access_key_id export AWS_SECRET_ACCESS_KEY=... # secret_access_key export AWS_DEFAULT_REGION=auto aws s3 cp report.pdf s3://<bucket>/reports/ --endpoint-url https://s3.uno.place
s3 = boto3.client("s3", endpoint_url="https://s3.uno.place", region_name="auto")
s3.upload_file("report.pdf", "<bucket>", "reports/report.pdf")In JavaScript pass endpoint: "https://s3.uno.place", region: "auto" and forcePathStyle: true. Uploads, downloads, listing, copy, delete, multipart and presigned GET and PUT work. Creating buckets, ACLs and versions do not: make buckets through the API or the console.
Upload to Uno with a presigned link
Your code makes a bucket, asks the Uno API for a link, and sends the file to the link. POST /api/v1/buckets with a name creates the bucket, and GET /api/v1/buckets lists yours with their ids. POST /api/v1/buckets/{id}/presign with a key and a method of put or get returns a url. A link lives up to 3,600 seconds. Make an API key in the console (it reaches sites, computers, storage and secrets; no billing and no new keys) and keep it in UNO_API_KEY.
import os
import requests
API = "https://console.uno.place/api/v1"
H = {"Authorization": f"Bearer {os.environ['UNO_API_KEY']}"}
# make the bucket once
bucket = requests.post(f"{API}/buckets", headers=H, json={"name": "assets"}).json()
# ask for a link, then send the file to it
link = requests.post(
f"{API}/buckets/{bucket['id']}/presign",
headers=H,
json={"key": "reports/report.pdf", "method": "put"},
).json()["url"]
with open("report.pdf", "rb") as f:
requests.put(link, data=f).raise_for_status()import { readFileSync } from "node:fs";
const API = "https://console.uno.place/api/v1";
const H = { Authorization: `Bearer ${process.env.UNO_API_KEY}`, "Content-Type": "application/json" };
const { buckets } = await (await fetch(`${API}/buckets`, { headers: H })).json();
const bucket = buckets.find((b) => b.name === "assets");
const { url } = await (
await fetch(`${API}/buckets/${bucket.id}/presign`, {
method: "POST",
headers: H,
body: JSON.stringify({ key: "reports/report.pdf", method: "put" }),
})
).json();
const res = await fetch(url, { method: "PUT", body: readFileSync("report.pdf") });
if (!res.ok) throw new Error(`upload failed: ${res.status}`);The answer says how big a file one link takes (max_bytes): 4.9 GB on most buckets, 50 MB on older ones. For a bigger file, send its size with the request and Uno returns part links instead. You send part n to link n, and Uno assembles the file about 30 seconds after the last part. On an older bucket, a single PUT over about 95 MB fails with HTTP 500, so large files go in parts.
What this does not do
- An S3 key cannot create or delete buckets, and Uno has no ACLs, object versions or presigned POST forms.
- Storage counts toward your plan: 25 GB on Small, 100 GB on Plus. Going over the included storage costs $0.01 per GB a month, and nothing is deleted for being over.
- A bucket is private. For a public website, publish the folder as a site instead.
Cloud storage in every plan
Files stay when you delete a computer. The bigger the plan, the more room.
Small
A Telegram bot, a VPN, a small backend
$5/ month
- 1 vCPU · 1 GB
- 15 GB disk
- 25 GB cloud storage
- 5 boosts a month
Plus
Agents that work with your laptop closed
$20/ month
- 4 vCPU · 4 GB
- 40 GB disk
- 100 GB cloud storage
- Uno Work in the cloud
Pro
A product in production, a second MacBook
$70/ month
- 16 vCPU · 16 GB
- 120 GB disk
- 300 GB cloud storage
- Speed of a MacBook Air M4
Max
A team and many agents at once
$200/ month
- 32 vCPU · 48 GB
- 300 GB disk
- 1,024 GB cloud storage
- 5 public IPv4
Add Uno AI to any plan: 40 h of AI work a month on Small, 100 h on Plus, unlimited on Max. Today: GLM-5.3 Flash and MiMo v2.6 Pro. Or sign in with your own Claude or ChatGPT.
Common problems
"Unable to locate credentials" in Python, "Could not load credentials from any providers" in JavaScript
env | grep AWS in the same terminal that runs the code. A key exported in another window does not count.AccessDenied
NoSuchBucket
endpoint_url for a service that is not Amazon's.SignatureDoesNotMatch
forcePathStyle: true in JavaScript.A big upload fails with HTTP 500
upload_file in Python and Upload in JavaScript do it for you. On an older Uno bucket, a single PUT over about 95 MB fails this way.