SDKs
The SDKs for Node.js, Python and Go do everything below for you: signing, integrity checks, retries, and large uploads and downloads in parallel parts. Install the one for your language:
| Language | Needs | Install |
|---|---|---|
| Node.js | 18.17+ | npm install @thailandhosting/object-storage |
| Python | 3.9+ | pip install thailandhosting-object-storage |
| Go | 1.23+ | go get github.com/mrzack99s/thailandhosting-object-storage-sdk-go |
Create the client with the endpoint of the bucket’s region and an access key from the Console. Keep the key in environment variables, not in your code, and reuse one client for the life of your program, since it keeps connections open.
import { Client, NotFoundError } from '@thailandhosting/object-storage';
const client = new Client({
endpoint: 'https://objects.thailandhosting.com/<region>',
accessKeyId: process.env.TH_ACCESS_KEY_ID,
secretAccessKey: process.env.TH_SECRET_ACCESS_KEY,
});
const bucket = client.bucket('my-bucket');
// Any size: large files go up and come down in parallel parts
await bucket.uploadFile('backups/db.dump', '/var/backups/db.dump');
await bucket.downloadFile('backups/db.dump', '/tmp/db.dump');
// Small files
await bucket.put('notes/hello.txt', 'Hello', { contentType: 'text/plain' });
console.log(await bucket.getText('notes/hello.txt'));
// Every file under a folder
for await (const obj of bucket.objects('backups/')) console.log(obj.key, obj.size);
// A link that works without a key for one hour
const link = await bucket.createLink('backups/db.dump', { expiresIn: 3600 });
console.log(link.url);
try {
await bucket.head('missing.txt');
} catch (e) {
if (e instanceof NotFoundError) console.log(e.code); // object_not_found
}import os
from thailandhosting_objectstorage import Client, NotFoundError
client = Client(
endpoint="https://objects.thailandhosting.com/<region>",
access_key_id=os.environ["TH_ACCESS_KEY_ID"],
secret_access_key=os.environ["TH_SECRET_ACCESS_KEY"],
)
bucket = client.bucket("my-bucket")
# Any size: large files go up and come down in parallel parts
bucket.upload_file("backups/db.dump", "/var/backups/db.dump")
bucket.download_file("backups/db.dump", "/tmp/db.dump")
# Small files
bucket.put("notes/hello.txt", "Hello", content_type="text/plain")
print(bucket.get_bytes("notes/hello.txt").decode())
# Every file under a folder
for obj in bucket.iter_objects("backups/"):
print(obj.key, obj.size)
# A link that works without a key for one hour
print(bucket.create_link("backups/db.dump", expires_in=3600).url)
try:
bucket.head("missing.txt")
except NotFoundError as e:
print(e.code) # object_not_foundpackage main
import (
"context"
"fmt"
"log"
"os"
"time"
objectstorage "github.com/mrzack99s/thailandhosting-object-storage-sdk-go"
)
func main() {
client, err := objectstorage.New(objectstorage.Config{
Endpoint: "https://objects.thailandhosting.com/<region>",
AccessKeyID: os.Getenv("TH_ACCESS_KEY_ID"),
SecretAccessKey: os.Getenv("TH_SECRET_ACCESS_KEY"),
})
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
bucket := client.Bucket("my-bucket")
// Any size: large files go up and come down in parallel parts
if _, err := bucket.UploadFile(ctx, "backups/db.dump", "/var/backups/db.dump", nil); err != nil {
log.Fatal(err)
}
if _, err := bucket.DownloadFile(ctx, "backups/db.dump", "/tmp/db.dump"); err != nil {
log.Fatal(err)
}
// Every file under a folder
for obj, err := range bucket.Objects(ctx, "backups/") {
if err != nil {
log.Fatal(err)
}
fmt.Println(obj.Key, obj.Size)
}
// A link that works without a key for one hour
link, err := bucket.CreateLink(ctx, "backups/db.dump", &objectstorage.LinkOptions{Expires: time.Hour})
if err != nil {
log.Fatal(err)
}
fmt.Println(link.URL)
}The SDKs also set public folders, give their addresses, and write a file only if it does not exist yet or has not changed since you read it:
// Anyone may read images/ without a key (the key must manage buckets)
await bucket.setPublicAccess([{ prefix: 'images/' }]);
console.log(bucket.publicUrl('images/logo.png'));
// Create a file only if it does not exist yet
await bucket.put('locks/job-42', 'me', { ifNoneMatch: '*' });
// Change it only if nobody changed it since you read it
const info = await bucket.head('config.json');
await bucket.put('config.json', next, { ifMatch: info.etag });Requests that fail because the service is briefly busy or the network dropped are tried again on their own. Every upload is checked against its digest, and a failed large upload is cancelled so its parts are not billed.
S3 tools
The AWS CLI, rclone, and apps that support S3 work by pointing them at the endpoint and using your access key, with the region set to the bucket’s region:
aws configure set aws_access_key_id YOUR_KEY_ID
aws configure set aws_secret_access_key YOUR_SECRET
aws s3 cp backup.tar.gz s3://your-bucket/ --endpoint-url https://objects.thailandhosting.com --region <region>Calling the API directly
The API is plain HTTP and JSON under /v1 on the endpoint, and uses the IETF standards instead of a format of its own:
| What | How |
|---|---|
| Signing in | Each request is signed with HTTP Message Signatures (RFC 9421) using your secret, so the secret itself is never sent. The request time must be within 5 minutes of ours. |
| Integrity | Uploads carry Content-Digest (RFC 9530). We check the body against it and refuse a mismatch, and downloads return Repr-Digest so you can check them too. |
| Safe updates | If-None-Match: * creates a file only if it does not exist yet, and If-Match changes or deletes it only if nobody changed it since you read it (RFC 9110). |
| Errors | Problem Details (RFC 9457): JSON with a code such as bucket_not_found. |
One upload or part is at most 5 GB, and a large upload can have up to 10,000 parts. A project can have up to 100 buckets and 50 access keys.
If something goes wrong
Requests fail with “invalid_signature” and mention the clock
The computer’s clock is more than 5 minutes off. Turn on automatic time (NTP) and try again.
An S3 tool says “SignatureDoesNotMatch”
Check the secret, and set the region to the bucket’s region, not a region of another cloud.
An upload fails with 412 “precondition_failed”
You asked to create the file only if it did not exist, or to change it only if it had not changed, and it had. Read the file again and decide whether to overwrite it.
Requests sometimes fail with 503 “busy” or “SlowDown”
The service is briefly busy. Wait the seconds in the Retry-After header and try again; the SDKs and S3 tools do this on their own.