Use the API, SDKs and S3 tools

The API follows the IETF HTTP standards, and the same buckets also work with S3 tools.

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:

LanguageNeedsInstall
Node.js18.17+npm install @thailandhosting/object-storage
Python3.9+pip install thailandhosting-object-storage
Go1.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.

Node.js
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
}
Python
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_found
Go
package 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:

Node.js
// 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:

Shell
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:

WhatHow
Signing inEach 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.
IntegrityUploads 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 updatesIf-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).
ErrorsProblem 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.

Still stuck? Let our team help

Open a ticket from the Console. Tell us the site or service, what you did and the message on screen. Our reply comes back in the same ticket, so everything stays in one place.