qencode-s3-sdk
v0.1.0
Published
Qencode-flavoured S3 toolkit (browser/Node) - framework-agnostic
Maintainers
Readme
qencode-s3-sdk
Qencode‑flavoured S3 toolkit for Node and browser (framework‑agnostic). It wraps AWS SDK v3 for the Qencode Media Storage endpoints and adds high‑level, ergonomic helpers for common bucket/prefix operations.
Highlights
- ESM + CJS builds with TypeScript types
- Simple, focused API: list, upload (with progress), head, copy/move/rename, folder ops
- Predictable behavior: functions return data or throw (raw AWS errors).
getItemInfois the only one that resolves{ exists: false }on 404.- Folder ops preserve empty subfolders by moving folder‑marker objects
Installation
npm i qencode-s3-sdk
# or: pnpm add qencode-s3-sdk
# or: yarn add qencode-s3-sdkThis package ships dual builds:
dist/index.js(ESM)dist/index.cjs(CJS)dist/index.d.ts(types)
Node ≥ 18 recommended.
Quick start (Node)
index.mjs
import 'dotenv/config';
import { listItems, uploadFile, getItemInfo } from 'qencode-s3-sdk';
const cfg = {
region: process.env.Q_REGION || 'us-west', // or 'eu-central'
access_id: process.env.Q_ACCESS_ID,
secret_key: process.env.Q_SECRET_KEY
};
const bucket = process.env.Q_BUCKET;
const prefix = process.env.Q_PREFIX || 'sdk-test/';
// 1) Upload with progress
const data = new Uint8Array(Buffer.from('hello from qencode-s3-sdk\n', 'utf8'));
await uploadFile(bucket, data, cfg, {
key: `${prefix}hello.txt`,
contentType: 'text/plain',
onProgress: p => process.stdout.write(`\r${p}%`)
});
process.stdout.write('\n');
// 2) List a prefix
const { items } = await listItems(bucket, cfg, prefix);
console.log(items.map(i => ({ name: i.name, type: i.type, size: i.size })));
// 3) HEAD an object
const info = await getItemInfo(bucket, `${prefix}hello.txt`, cfg);
console.log(info.exists ? info.metadata : { exists: false });CommonJS
require('dotenv/config');
const { listItems } = require('qencode-s3-sdk');React usage
Production rule: Do not embed Qencode credentials in the browser. Call the SDK on your server (Express/Fastify/etc.), expose minimal endpoints (
/list,/upload, …), and call those endpoints from React.Below are two dev‑only React patterns for quick local testing. Use them only during development.
A) Dev‑only React (config from env)
.env.local
VITE_Q_REGION=us-west
VITE_Q_ACCESS_ID=your_access_id
VITE_Q_SECRET_KEY=your_secret_key
VITE_Q_BUCKET=your_bucket
VITE_Q_PREFIX=sdk-test/App.tsx
import { useState } from 'react';
import { uploadFile, listItems, type QencodeS3Config } from 'qencode-s3-sdk';
export default function App() {
const [items, setItems] = useState<any[]>([]);
const cfg: QencodeS3Config = {
region: import.meta.env.VITE_Q_REGION || 'us-west',
access_id: import.meta.env.VITE_Q_ACCESS_ID,
secret_key: import.meta.env.VITE_Q_SECRET_KEY
};
const bucket = import.meta.env.VITE_Q_BUCKET as string;
const prefix = (import.meta.env.VITE_Q_PREFIX as string) || 'sdk-test/';
async function onUpload(e: React.ChangeEvent<HTMLInputElement>) {
const f = e.target.files?.[0];
if (!f || !bucket) return;
await uploadFile(bucket, f, cfg, {
key: `${prefix}${f.name}`,
contentType: f.type || 'application/octet-stream',
onProgress: p => console.log('progress', p)
});
const { items } = await listItems(bucket, cfg, prefix);
setItems(items);
}
return (
<main style={{ padding: 24 }}>
<h3>Dev‑only React example</h3>
<input type="file" onChange={onUpload} />
<ul>
{items.map(i => <li key={i.name}>{i.type === 'folder' ? '📁' : '📄'} {i.name}</li>)}
</ul>
<p style={{ color: '#b00' }}>⚠️ Dev‑only. Do not use this pattern in production.</p>
</main>
);
}B) Dev‑only React (config via API call to a local server)
server.js (Express)
import 'dotenv/config';
import express from 'express';
const app = express();
app.get('/api/qencode/config', (req, res) => {
res.json({
region: process.env.Q_REGION || 'us-west',
access_id: process.env.Q_ACCESS_ID,
secret_key: process.env.Q_SECRET_KEY,
bucket: process.env.Q_BUCKET,
prefix: process.env.Q_PREFIX || 'sdk-test/'
});
});
app.listen(3001, () => console.log('dev config server on :3001'));App.tsx
import { useEffect, useState } from 'react';
import { listItems, uploadFile, type QencodeS3Config } from 'qencode-s3-sdk';
export default function App() {
const [cfg, setCfg] = useState<QencodeS3Config | null>(null);
const [bucket, setBucket] = useState('');
const [prefix, setPrefix] = useState('sdk-test/');
const [items, setItems] = useState<any[]>([]);
useEffect(() => {
fetch('http://localhost:3001/api/qencode/config')
.then(r => r.json())
.then(c => {
setCfg({ region: c.region, access_id: c.access_id, secret_key: c.secret_key });
setBucket(c.bucket);
setPrefix(c.prefix);
})
.catch(console.error);
}, []);
async function refresh() {
if (!cfg || !bucket) return;
const { items } = await listItems(bucket, cfg, prefix);
setItems(items);
}
async function onUpload(e: React.ChangeEvent<HTMLInputElement>) {
const f = e.target.files?.[0];
if (!cfg || !bucket || !f) return;
await uploadFile(bucket, f, cfg, {
key: `${prefix}${f.name}`,
contentType: f.type || 'application/octet-stream'
});
refresh();
}
return (
<main style={{ padding: 24 }}>
<h3>Dev‑only React (config via API)</h3>
<input type="file" onChange={onUpload} />
<button onClick={refresh}>Refresh</button>
<ul>{items.map(i => <li key={i.name}>{i.name}</li>)}</ul>
<p style={{ color: '#b00' }}>⚠️ Dev‑only — do not expose credentials in production.
In production, call the SDK on your server and return only the data you need.</p>
</main>
);
}API (summary)
Types
export const REGIONS = ['eu-central', 'us-west'] as const;
export type QencodeRegion = (typeof REGIONS)[number];
export interface QencodeS3Config {
region: QencodeRegion;
access_id: string;
secret_key: string;
/** Advanced: override the endpoint base (default: "s3.qencode.com"). */
endpointBase?: string;
}
export type FileOpResult = { message: string; changed: boolean };
export type FolderOpResult = { message: string; moved: number };Client helpers
import { createQencodeS3Client, virtualHostedUrl } from 'qencode-s3-sdk';
const s3 = createQencodeS3Client(cfg); // low-level AWS S3Client if you need it
const url = virtualHostedUrl('my-bucket', 'folder/a b.txt', cfg);
// → https://my-bucket.us-west.s3.qencode.com/folder/a%20b.txtStorage
All functions throw on failure (reject with raw AWS errors), unless noted.
listItems(bucket, cfg, prefix = "", delimiter = "/")→{ items }(folders + files)uploadFile(bucket, data, cfg, opts?)→{ path, result }(progress callback supported)getItemInfo(bucket, key, cfg)→{ exists: false }on 404; otherwise{ exists: true, metadata }countItems(bucket, cfg, prefix = "")→{ itemCount }totalSize(bucket, cfg, prefix = "")→{ totalSize }(bytes)createFolder(bucket, prefix, cfg)→{ message }(zero‑byte marker viaPutObject)deleteFile(bucket, key, cfg)→{ message }deleteFolder(bucket, prefix, cfg)→{ message }(throws if nothing matched)copyFile(bucket, srcKey, dstKey, cfg)→{ message }(multipart for >5 GiB)renameFile(bucket, oldKey, newKey, cfg)→{ message, changed }(no‑op if keys equal)moveFile(bucket, oldKey, newKey, cfg)→{ message, changed }renameFolder(bucket, oldPrefix, newPrefix, cfg)→{ message, moved }(preserves empty subfolders)moveFolder(bucket, oldPrefix, newParentPrefix, cfg)→{ message, moved }
Prefix rules & folder semantics
- All prefix parameters are normalized: no leading
/, guaranteed trailing/. - S3 has no real folders; folder UIs rely on prefix and optional zero‑byte folder markers (
<prefix>/). listItemsfilters markers from files, but folder ops move/delete markers so your empty subfolders are preserved.
Error model
All functions return data on success and throw raw AWS errors on failure (network/auth/403/etc.).
getItemInfois the only function that normalizes 404 to{ exists: false }.Validation errors you might see:
Destination is inside the source folder.(rename/move folder)Folder does not exist or is empty.(rename/move folder when nothing matched)
Local testing
Use the built‑in tests (they read credentials from .env).
cp .env.example .env # fill Q_ACCESS_ID, Q_SECRET_KEY, Q_REGION, Q_BUCKET
npm run build
node tests/listItems.test.js
# seed data when needed
Q_SEED=1 node tests/renameFolder.test.jsTest packaging without publishing
npm run build
npm publish --dry-run
npm pack # → qencode-s3-sdk-x.y.z.tgz
mkdir /tmp/sdk-tmp && cd /tmp/sdk-tmp
npm init -y
npm i /path/to/qencode-s3-sdk-x.y.z.tgzLicense
MIT © Qencode
