Skip to main content
QingStor (QingCloud Object Storage) exposes an S3-compatible API with AWS Signature V4 against https://s3.<zone>.qingstor.com. MIRAGE derives this endpoint from your zone automatically (the zone is passed as region). Credentials (QingCloud API access key pair) are created the same way in both runtimes, see QingStor Credentials.

Node (server-side)

pnpm add @struktoai/mirage-node
import { MountMode, QingStorResource, Workspace } from '@struktoai/mirage-node'

const qs = new QingStorResource({
  bucket: process.env.QINGSTOR_BUCKET!,
  region: process.env.QINGSTOR_ZONE!,
  accessKeyId: process.env.QINGSTOR_ACCESS_KEY_ID!,
  secretAccessKey: process.env.QINGSTOR_SECRET_ACCESS_KEY!,
})

const ws = new Workspace({ '/bucket/': qs }, { mode: MountMode.READ })
const res = await ws.execute('ls /bucket/')
console.log(res.stdoutText)

Browser (presigned URLs)

pnpm add @struktoai/mirage-browser
The browser QingStorResource is secret-free, your backend signs each operation using your QingStor keys and returns a URL. QingStor accepts AWS Signature V4, so @aws-sdk/s3-request-presigner works, pointed at the QingStor endpoint.

1. Server: sign URLs with the QingStor endpoint

import {
  CopyObjectCommand,
  DeleteObjectCommand,
  GetObjectCommand,
  HeadObjectCommand,
  ListObjectsV2Command,
  PutObjectCommand,
  S3Client,
} from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'

const ZONE = process.env.QINGSTOR_ZONE!

const client = new S3Client({
  region: ZONE,
  endpoint: `https://s3.${ZONE}.qingstor.com`,
  credentials: {
    accessKeyId: process.env.QINGSTOR_ACCESS_KEY_ID!,
    secretAccessKey: process.env.QINGSTOR_SECRET_ACCESS_KEY!,
  },
})
const BUCKET = process.env.QINGSTOR_BUCKET!

app.post('/presign/qingstor', async (req, res) => {
  const { path, op, opts } = req.body
  const key = path.replace(/^\/+/, '')
  const ttl = typeof opts?.ttlSec === 'number' ? opts.ttlSec : 300
  let cmd
  switch (op) {
    case 'GET':    cmd = new GetObjectCommand({ Bucket: BUCKET, Key: key }); break
    case 'PUT':    cmd = new PutObjectCommand({ Bucket: BUCKET, Key: key, ContentType: opts?.contentType }); break
    case 'HEAD':   cmd = new HeadObjectCommand({ Bucket: BUCKET, Key: key }); break
    case 'DELETE': cmd = new DeleteObjectCommand({ Bucket: BUCKET, Key: key }); break
    case 'LIST':   cmd = new ListObjectsV2Command({
      Bucket: BUCKET,
      Prefix: opts?.listPrefix,
      Delimiter: opts?.listDelimiter,
      ContinuationToken: opts?.listContinuationToken,
    }); break
    case 'COPY':   cmd = new CopyObjectCommand({
      Bucket: BUCKET,
      Key: key,
      CopySource: `${BUCKET}/${opts?.copySource}`,
    }); break
  }
  res.json({ url: await getSignedUrl(client, cmd, { expiresIn: ttl }) })
})

2. Browser: wire it up

import { MountMode, QingStorResource, Workspace } from '@struktoai/mirage-browser'

const qs = new QingStorResource({
  bucket: 'my-bucket',
  region: 'pek3a',
  presignedUrlProvider: async (path, op, opts) => {
    const r = await fetch('/presign/qingstor', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ path, op, opts }),
    })
    const { url } = await r.json()
    return url
  },
})

const ws = new Workspace({ '/bucket/': qs }, { mode: MountMode.READ })
region on the browser config is only used for display/logging; the actual endpoint is baked into the presigned URLs your backend returns.

3. CORS

Configure CORS rules in the QingCloud console: QingStor -> your bucket -> Basic Settings -> CORS. Allow your dev/production origins with methods GET, PUT, HEAD, DELETE, POST, allowed headers *, and expose headers ETag, Content-Length, Content-Type, Last-Modified. See the QingStor resource docs for the equivalent Python wiring.