@weconjs/client-fm
v0.1.2
Published
Browser-side file manager client with chunked uploads, caching, gallery, and headless file viewer
Maintainers
Readme
@weconjs/client-fm
A modern, zero-dependency TypeScript browser package for file management. Handles chunked uploads (bypassing server body-size limits), batch uploads, file viewing with state management, gallery listing, downloads, and metadata caching — all through a clean singleton API.
Table of Contents
- Installation
- Quick Start
- Configuration
- API Reference
- Use Cases
- 1. Single File Upload with Progress
- 2. Upload with Visibility and Metadata
- 3. Pause, Resume, and Abort Upload
- 4. Upload Multiple Files
- 5. View an Image
- 6. View a Video
- 7. View a PDF
- 8. Play Audio
- 9. Reactive Viewer with Loading States
- 10. Browse Gallery (All Files)
- 11. Browse Gallery by User
- 12. Gallery with Filters and Sorting
- 13. Download a File
- 14. Get File Metadata
- 15. Delete a File
- 16. Update Token (Auth Refresh)
- 17. Initialise Without Token, Authenticate Later
- 18. Error Handling
- 19. Cache Management
- 20. Cleanup and Destroy
- Custom Endpoints (Advanced)
- Backend API Endpoints (Defaults)
- Express Backend Demo
- Types
- License
Installation
npm / yarn
yarn add @weconjs/client-fm
# or
npm install @weconjs/client-fmCDN (browser global)
<!-- unpkg -->
<script src="https://unpkg.com/@weconjs/client-fm"></script>
<!-- jsdelivr -->
<script src="https://cdn.jsdelivr.net/npm/@weconjs/client-fm"></script>The CDN build exposes a ClientFM global. All exports are available as properties:
<script src="https://unpkg.com/@weconjs/client-fm"></script>
<script>
const fm = ClientFM.FileManager.create({
baseUrl: 'https://api.example.com/fm',
token: 'my-token',
});
</script>| Build | File | Size (min) | Size (gzip) |
| ------- | --------------------- | ---------- | ----------- |
| ESM | dist/index.js | ~45 KB | ~11 KB |
| CJS | dist/index.cjs | ~45 KB | ~11 KB |
| CDN | dist/index.global.js| ~19 KB | ~5 KB |
Quick Start
import { FileManager } from '@weconjs/client-fm';
// 1. Create a singleton instance
const fm = FileManager.create({
baseUrl: 'https://api.example.com/fm',
token: 'your-jwt-token',
});
// 2. Load server config (chunk size, allowed types, etc.)
await fm.ready();
// 3. Upload a file
const uploader = fm.upload(fileInput.files[0], { visibility: 'public' });
uploader.on('progress', ({ percentage }) => console.log(`${percentage}%`));
const result = await uploader.start();
console.log('Uploaded:', result.url);Configuration
interface ClientConfig {
/** Base URL of the file manager API. */
baseUrl: string;
/** Bearer token for authentication (can be set later via setToken()). */
token?: string;
/** Max retry attempts for failed requests (default: 3). */
maxRetries?: number;
/** Base delay in ms between retries, doubled each attempt (default: 1000). */
retryDelay?: number;
/** Max entries in the LRU metadata cache (default: 100). */
cacheMaxSize?: number;
/** TTL in ms for cached entries (default: 300000 — 5 min). */
cacheTTL?: number;
/**
* Advanced: customise individual API endpoint paths, methods, payloads,
* query parameters, headers, and response transformations.
* Only specify the endpoints you want to override — all others keep defaults.
* See "Custom Endpoints (Advanced)" section below.
*/
endpoints?: Partial<EndpointConfig>;
}API Reference
FileManager
The main entry point. Created once, used everywhere.
| Method | Returns | Description |
|--------|---------|-------------|
| FileManager.create(config) | FileManager | Create and register the singleton |
| FileManager.getInstance() | FileManager | Retrieve the existing singleton |
| fm.init() | Promise<void> | Eagerly fetch server chunk config |
| fm.ready() | Promise<FileManager> | Ensure server config is loaded (idempotent) |
| fm.setToken(token) | void | Update the auth token at any time |
| fm.upload(file, options?) | ChunkedUploader | Start a chunked upload |
| fm.uploadMany(entries) | BatchUploader | Start a multi-file batch upload |
| fm.download(fileId) | Promise<Blob> | Download a file as a Blob |
| fm.getFileInfo(fileId) | Promise<FileRecord> | Get file metadata (cached) |
| fm.gallery(query?) | Promise<GalleryResponse> | List files with filters |
| fm.userGallery(userId, query?) | Promise<GalleryResponse> | List files by user |
| fm.view(fileId, params?) | FileViewer | Create a headless file viewer |
| fm.deleteFile(fileId) | Promise<void> | Delete a file |
| fm.clearCache() | void | Clear all cached metadata |
| fm.destroy() | void | Destroy the singleton and cleanup |
ChunkedUploader
Returned by fm.upload(). Manages the lifecycle of a single file upload.
| Method / Property | Returns | Description |
|-------------------|---------|-------------|
| uploader.start() | Promise<UploadCompleteResponse> | Start the upload |
| uploader.pause() | void | Pause after the current chunk |
| uploader.resume() | void | Resume a paused upload |
| uploader.abort() | void | Cancel the upload |
| uploader.status | UploadStatus | Current status |
| uploader.progress | number | Percentage 0–100 |
| uploader.fileId | string \| null | Server-assigned file ID |
Events:
| Event | Payload | When |
|-------|---------|------|
| progress | { loaded, total, percentage, chunkIndex } | After each chunk succeeds |
| chunk-complete | { chunkIndex, totalChunks } | After each chunk succeeds |
| complete | UploadCompleteResponse | All chunks uploaded and finalised |
| error | { error, chunkIndex?, retriesLeft } | A chunk failed (may be retried) |
| abort | void | Upload was aborted |
BatchUploader
Returned by fm.uploadMany(). Manages multiple sequential file uploads.
| Method / Property | Returns | Description |
|-------------------|---------|-------------|
| batch.start() | Promise<BatchFileResult[]> | Start all uploads |
| batch.abort() | void | Abort all uploads |
| batch.getUploader(index) | ChunkedUploader \| undefined | Access individual uploader |
| batch.isRunning | boolean | Whether the batch is in progress |
| batch.totalFiles | number | Total number of files |
Events:
| Event | Payload | When |
|-------|---------|------|
| progress | { completedFiles, totalFiles, percentage, currentFilePercentage, currentFileIndex } | Progress on any file |
| file-complete | { index, result } | A file finished uploading |
| file-error | { index, error } | A file failed (batch continues) |
| complete | BatchFileResult[] | All files processed |
| abort | void | Batch was aborted |
FileViewer
Returned by fm.view(). Headless file viewer with state management.
| Method / Property | Returns | Description |
|-------------------|---------|-------------|
| viewer.load() | Promise<ViewResult> | Fetch metadata, blob, create URL |
| viewer.reload() | Promise<ViewResult> | Re-fetch bypassing cache |
| viewer.dispose() | void | Must call — revokes object URL |
| viewer.state | ViewResult | Full state snapshot |
| viewer.status | ViewStatus | 'idle' \| 'loading' \| 'loaded' \| 'error' |
| viewer.src | string \| null | Object URL ready for src attrs |
| viewer.mediaCategory | MediaCategory | 'image' \| 'video' \| 'audio' \| 'pdf' \| 'text' \| 'unknown' |
Events:
| Event | Payload | When |
|-------|---------|------|
| state-change | ViewResult | On every state transition |
| loaded | ViewResult | When file is loaded and ready |
| error | { error, fileId } | When loading fails |
ViewFileParams:
interface ViewFileParams {
as?: MediaCategory; // Force media category
width?: number; // Image: desired width
height?: number; // Image: desired height
quality?: number; // Image: quality 1–100
stream?: boolean; // Video/Audio: request streamable URL
page?: number; // PDF: specific page
headers?: Record<string, string>; // Custom headers (e.g. Range)
}Use Cases
1. Single File Upload with Progress
const fm = FileManager.create({ baseUrl: 'https://api.example.com/fm', token: 'jwt' });
await fm.ready();
const uploader = fm.upload(fileInput.files[0]);
uploader.on('progress', ({ percentage, chunkIndex }) => {
progressBar.style.width = `${percentage}%`;
console.log(`Chunk ${chunkIndex} done — ${percentage}%`);
});
uploader.on('complete', (result) => {
console.log('Done!', result.url);
});
const result = await uploader.start();2. Upload with Visibility and Metadata
const uploader = fm.upload(file, {
visibility: 'public',
metadata: {
description: 'Profile photo',
tags: ['avatar', 'user-123'],
uploadedBy: 'user-123',
},
});
const result = await uploader.start();3. Pause, Resume, and Abort Upload
const uploader = fm.upload(largeFile);
uploader.start(); // don't await — control flow independently
// Pause after the current chunk completes
pauseBtn.onclick = () => uploader.pause();
// Resume
resumeBtn.onclick = () => uploader.resume();
// Abort entirely (cancels in-flight request)
cancelBtn.onclick = () => uploader.abort();
// Check status at any time
console.log(uploader.status); // 'uploading' | 'paused' | 'aborted' | ...
console.log(uploader.progress); // 0–1004. Upload Multiple Files
const batch = fm.uploadMany([
{ file: photoFile, options: { visibility: 'public' } },
{ file: documentFile, options: { visibility: 'private', metadata: { type: 'invoice' } } },
{ file: videoFile },
]);
// Aggregate progress across all files
batch.on('progress', ({ completedFiles, totalFiles, percentage }) => {
progressLabel.textContent = `${completedFiles}/${totalFiles} files — ${percentage}%`;
});
// Per-file callbacks
batch.on('file-complete', ({ index, result }) => {
console.log(`File ${index} uploaded:`, result.url);
});
batch.on('file-error', ({ index, error }) => {
console.error(`File ${index} failed:`, error.message);
// Batch continues to the next file automatically
});
const results = await batch.start();
// Inspect results
results.forEach(({ file, result, error }) => {
if (error) console.error(`${file.name} failed:`, error);
else console.log(`${file.name} uploaded:`, result!.url);
});5. View an Image
const viewer = fm.view('file-id-123', {
as: 'image',
width: 800,
quality: 85,
});
const { status, src } = await viewer.load();
if (status === 'loaded') {
imgElement.src = src!;
}
// IMPORTANT: dispose when done (e.g. on component unmount)
viewer.dispose();6. View a Video
const viewer = fm.view('video-file-id', { stream: true });
const result = await viewer.load();
if (result.status === 'loaded') {
videoElement.src = result.src!;
videoElement.play();
}
// Cleanup
viewer.dispose();7. View a PDF
const viewer = fm.view('pdf-file-id', { page: 1 });
const { status, src } = await viewer.load();
if (status === 'loaded') {
iframeElement.src = src!;
}
viewer.dispose();8. Play Audio
const viewer = fm.view('audio-file-id');
const { status, src, mediaCategory } = await viewer.load();
if (status === 'loaded' && mediaCategory === 'audio') {
audioElement.src = src!;
audioElement.play();
}
viewer.dispose();9. Reactive Viewer with Loading States
The viewer emits state-change on every transition, making it perfect for reactive UIs:
const viewer = fm.view('file-id-456');
viewer.on('state-change', ({ status, src, mediaCategory, error }) => {
switch (status) {
case 'loading':
spinner.style.display = 'block';
content.style.display = 'none';
errorEl.style.display = 'none';
break;
case 'loaded':
spinner.style.display = 'none';
content.style.display = 'block';
if (mediaCategory === 'image') imgEl.src = src!;
if (mediaCategory === 'video') videoEl.src = src!;
if (mediaCategory === 'audio') audioEl.src = src!;
if (mediaCategory === 'pdf') iframeEl.src = src!;
break;
case 'error':
spinner.style.display = 'none';
errorEl.style.display = 'block';
errorEl.textContent = error!.message;
break;
}
});
viewer.load();
// Reload with fresh data (bypasses cache)
refreshBtn.onclick = () => viewer.reload();
// Cleanup
unmount(() => viewer.dispose());10. Browse Gallery (All Files)
const gallery = await fm.gallery({ page: 1, limit: 20 });
console.log(`${gallery.total} files, page ${gallery.page}/${gallery.totalPages}`);
gallery.data.forEach((file) => {
console.log(file.fileName, file.mimeType, file.size, file.url);
});11. Browse Gallery by User
const userFiles = await fm.userGallery('user-123', {
page: 1,
limit: 10,
});
userFiles.data.forEach((file) => {
console.log(file.fileName, file.visibility);
});12. Gallery with Filters and Sorting
// Only public images, sorted by size descending
const images = await fm.gallery({
visibility: 'public',
mimeType: 'image/*',
sortBy: 'size',
sortOrder: 'desc',
page: 1,
limit: 50,
});13. Download a File
const blob = await fm.download('file-id-789');
// Trigger browser download
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'my-file.pdf';
a.click();
URL.revokeObjectURL(url);14. Get File Metadata
const file = await fm.getFileInfo('file-id-789');
console.log(file.fileName); // "report.pdf"
console.log(file.mimeType); // "application/pdf"
console.log(file.size); // 1048576
console.log(file.visibility); // "private"
console.log(file.url); // "https://..."
console.log(file.metadata); // { description: "..." }
console.log(file.createdAt); // "2025-01-15T10:30:00Z"15. Delete a File
await fm.deleteFile('file-id-789');16. Update Token (Auth Refresh)
// Create instance without a token
const fm = FileManager.create({
baseUrl: 'https://api.example.com/fm',
});
// After user logs in
fm.setToken(authResponse.accessToken);
// After token refresh
fm.setToken(newAccessToken);17. Initialise Without Token, Authenticate Later
const fm = FileManager.create({
baseUrl: 'https://api.example.com/fm',
});
// Public gallery works without a token (if server allows)
const gallery = await fm.gallery({ visibility: 'public' });
// After login, set token and unlock protected features
fm.setToken(jwt);
await fm.ready(); // fetch server config for uploads
const uploader = fm.upload(file, { visibility: 'private' });
await uploader.start();18. Error Handling
import { FileManager } from '@weconjs/client-fm';
const fm = FileManager.create({ baseUrl: 'https://api.example.com/fm', token: 'jwt' });
try {
await fm.ready();
} catch (err) {
console.error('Failed to connect to server:', err);
}
// Upload with granular error handling
const uploader = fm.upload(file);
uploader.on('error', ({ error, chunkIndex, retriesLeft }) => {
console.warn(
`Chunk ${chunkIndex} failed: ${error.message}. ` +
`${retriesLeft} retries remaining.`
);
});
try {
const result = await uploader.start();
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') {
console.log('Upload was aborted');
} else {
console.error('Upload failed permanently:', err);
}
}19. Cache Management
Metadata and gallery results are cached automatically with LRU + TTL:
// First call: fetches from server
const file1 = await fm.getFileInfo('file-id');
// Second call: served from cache (instant)
const file2 = await fm.getFileInfo('file-id');
// Force clear all cached data
fm.clearCache();
// Viewer also supports cache bypass
const viewer = fm.view('file-id');
await viewer.load(); // uses cache
await viewer.reload(); // bypasses cache20. Cleanup and Destroy
// Destroy the singleton (clears cache, resets state)
fm.destroy();
// Access anywhere via singleton
const fm = FileManager.getInstance();
// Create a fresh instance
const newFm = FileManager.create({ baseUrl: '...' });Custom Endpoints (Advanced)
Why?
By default, @weconjs/client-fm expects your backend to implement specific endpoint paths (e.g. POST /files/init, GET /files/:fileId/download). This works great with the companion backend, but if you have an existing API with different routes, query parameter names, or response shapes, you no longer need to change your backend — you can adapt the client instead.
Overview
Pass an endpoints object in your config to override any endpoint. Each endpoint supports 6 customisation hooks:
| Hook | What it controls | Default |
|------|-----------------|---------|
| path | URL path (string with :fileId/:userId placeholders, or a function) | Built-in paths |
| method | HTTP method | Built-in methods |
| transformQuery | Remap/rename/add query parameters | Pass-through |
| transformPayload | Transform the JSON request body | Pass-through |
| transformHeaders | Add/modify per-endpoint request headers | Pass-through |
| transformResponse | Transform the JSON response into the shape the SDK expects | Pass-through |
You only need to specify what you want to change — everything else keeps the default behaviour. Existing code is 100% unaffected if you don't use endpoints.
interface EndpointDescriptor {
path?: string | ((params: EndpointParams) => string);
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
transformPayload?: (payload: any) => any;
transformQuery?: (query: Record<string, any>) => Record<string, any>;
transformHeaders?: (headers: Record<string, string>) => Record<string, string>;
transformResponse?: (data: any) => any;
}Endpoint Keys Reference
| Key | Default Path | Default Method | Description |
|-----|-------------|----------------|-------------|
| config | /files/config | GET | Server chunk configuration |
| uploadInit | /files/init | POST | Initialise a chunked upload |
| uploadChunk | /files/:fileId/chunks | POST | Upload a single chunk |
| uploadComplete | /files/:fileId/complete | POST | Finalise an upload |
| fileInfo | /files/:fileId | GET | Get file metadata |
| fileDownload | /files/:fileId/download | GET | Download file content |
| fileDelete | /files/:fileId | DELETE | Delete a file |
| fileList | /files | GET | List files (gallery) |
| shareFile | /files/:fileId/share | POST | Share with a user |
| shareBatch | /files/:fileId/share/batch | POST | Share with multiple users |
| updatePermissions | /files/:fileId/permissions/:userId | PUT | Update user permissions |
| revokePermissions | /files/:fileId/permissions/:userId | DELETE | Revoke user access |
| getPermissions | /files/:fileId/permissions | GET | List file permissions |
Custom Paths
const fm = FileManager.create({
baseUrl: 'https://api.example.com',
endpoints: {
// Simple string — just change the route
uploadInit: { path: '/api/v2/uploads/start' },
// String with placeholders — :fileId is interpolated automatically
uploadChunk: { path: '/api/v2/uploads/:fileId/parts' },
// Function — full control
fileDownload: {
path: (params) => `/cdn/assets/${params.fileId}/raw`,
},
},
});Custom Query Parameters
Remap, rename, or add query parameters on any endpoint. Especially useful for the gallery/file list endpoint:
const fm = FileManager.create({
baseUrl: 'https://api.example.com',
endpoints: {
fileList: {
// Your backend uses "offset/count" instead of "page/limit"
transformQuery: (query) => ({
offset: ((query.page ?? 1) - 1) * (query.limit ?? 20),
count: query.limit ?? 20,
sort_field: query.sortBy,
sort_dir: query.sortOrder,
owner: query.userId,
type: query.mimeType,
access: query.visibility,
}),
},
fileDownload: {
// Rename image transform params
transformQuery: (query) => ({
width: query.w,
height: query.h,
quality: query.q,
pg: query.page,
...query,
}),
},
},
});
// Usage stays exactly the same — the SDK calls your transform automatically
const gallery = await fm.gallery({ page: 3, limit: 10, sortBy: 'size' });
// → GET /files?offset=20&count=10&sort_field=sizeCustom Request Payloads
Transform the JSON body before it's sent. Useful when your backend expects different field names:
const fm = FileManager.create({
baseUrl: 'https://api.example.com',
endpoints: {
uploadInit: {
transformPayload: (body) => ({
// Your backend uses different field names
name: body.fileName,
size: body.fileSize,
type: body.mimeType,
access: body.visibility,
meta: body.metadata,
chunks: body.totalChunks,
}),
},
shareFile: {
transformPayload: (body) => ({
target_user: body.userId,
access_level: body.permissions,
}),
},
},
});
// Usage stays the same
const uploader = fm.upload(file, { visibility: 'public' });
// → POST body: { name: "file.jpg", size: 1024, type: "image/jpeg", access: "public", chunks: 2 }Custom Response Parsing
Transform server responses into the shape the SDK expects internally:
const fm = FileManager.create({
baseUrl: 'https://api.example.com',
endpoints: {
// Your server returns { id, token } instead of { fileId, uploadToken }
uploadInit: {
transformResponse: (raw) => ({
fileId: raw.id,
uploadToken: raw.token,
}),
},
// Your gallery uses a different pagination envelope
fileList: {
transformResponse: ({ data, meta }) => ({
data: data, // array of file records
total: meta?.total_count ?? 0,
page: meta?.current_page ?? 1,
limit: meta?.per_page ?? 20,
totalPages: meta?.last_page ?? 1,
}),
},
// Your file metadata has different field names
fileInfo: {
transformResponse: (raw) => ({
id: raw._id,
fileName: raw.original_name,
mimeType: raw.content_type,
size: raw.bytes,
visibility: raw.access,
url: raw.download_url,
metadata: raw.meta,
createdAt: raw.created,
updatedAt: raw.modified,
}),
},
},
});Custom Headers
Add per-endpoint headers (e.g. API keys, custom auth):
const fm = FileManager.create({
baseUrl: 'https://api.example.com',
endpoints: {
uploadChunk: {
transformHeaders: (headers) => ({
...headers,
'X-Upload-Token': 'special-upload-key',
}),
},
},
});Custom HTTP Methods
Override the HTTP method for any endpoint:
const fm = FileManager.create({
baseUrl: 'https://api.example.com',
endpoints: {
// Use PATCH instead of PUT for permission updates
updatePermissions: { method: 'PATCH' },
},
});Full Example: Adapting to a Custom Backend
Here's a complete example showing how to connect @weconjs/client-fm to a backend with completely different routes and conventions:
import { FileManager } from '@weconjs/client-fm';
const fm = FileManager.create({
baseUrl: 'https://api.myapp.com/v3',
token: 'my-jwt',
endpoints: {
config: {
path: '/storage/settings',
transformResponse: (raw) => ({
chunkSize: raw.chunk_bytes,
maxFileSize: raw.max_bytes,
allowedMimeTypes: raw.accepted_types,
}),
},
uploadInit: {
path: '/storage/upload/new',
transformPayload: (b) => ({
name: b.fileName,
bytes: b.fileSize,
content_type: b.mimeType,
access: b.visibility ?? 'private',
parts: b.totalChunks,
}),
transformResponse: (r) => ({
fileId: r.upload_id,
uploadToken: r.session_token,
}),
},
uploadChunk: {
path: (p) => `/storage/upload/${p.fileId}/part`,
},
uploadComplete: {
path: (p) => `/storage/upload/${p.fileId}/finish`,
transformResponse: (r) => ({
fileId: r.id,
url: r.public_url,
size: r.bytes,
mimeType: r.content_type,
}),
},
fileList: {
path: '/storage/browse',
transformQuery: (q) => ({
offset: ((q.page ?? 1) - 1) * (q.limit ?? 20),
count: q.limit ?? 20,
owner: q.userId,
type: q.mimeType,
}),
transformResponse: ({ data, meta }) => ({
data: data.map((f: any) => ({
id: f._id,
fileName: f.name,
mimeType: f.type,
size: f.bytes,
visibility: f.access,
url: f.link,
createdAt: f.created,
updatedAt: f.updated,
})),
total: meta.total,
page: meta.page,
limit: meta.per_page,
totalPages: meta.pages,
}),
},
fileInfo: {
path: '/storage/meta/:fileId',
transformResponse: (r) => ({
id: r._id,
fileName: r.name,
mimeType: r.type,
size: r.bytes,
visibility: r.access,
url: r.link,
createdAt: r.created,
updatedAt: r.updated,
}),
},
fileDownload: {
path: '/storage/raw/:fileId',
},
fileDelete: {
path: '/storage/remove/:fileId',
},
},
});
await fm.ready();
// Everything works exactly the same from here — the SDK handles the mapping
const uploader = fm.upload(file, { visibility: 'public' });
uploader.on('progress', ({ percentage }) => console.log(`${percentage}%`));
const result = await uploader.start();
console.log('Uploaded:', result.url);Backend API Endpoints (Defaults)
Below is the full specification of the API endpoints this package expects your backend to implement by default. If your backend uses different routes or conventions, see Custom Endpoints (Advanced) above to remap them without changing your server.
GET /config
Returns the server's chunk configuration. Called by fm.init() / fm.ready().
Headers:
Authorization: Bearer <token>Response 200 OK:
{
"chunkSize": 512000,
"maxFileSize": 5242880,
"allowedMimeTypes": ["image/jpeg", "image/png", "application/pdf", "video/mp4"]
}| Field | Type | Description |
|-------|------|-------------|
| chunkSize | number | Size of each chunk in bytes |
| maxFileSize | number | Max total file size allowed |
| allowedMimeTypes | string[]? | Whitelist of MIME types (empty = all) |
POST /files/init
Initialise a new chunked upload. Called once before any chunks are sent.
Headers:
Authorization: Bearer <token>
Content-Type: application/jsonRequest body:
{
"fileName": "report.pdf",
"fileSize": 15728640,
"mimeType": "application/pdf",
"visibility": "private",
"metadata": {
"description": "Q4 Financial Report",
"department": "finance"
},
"totalChunks": 31
}| Field | Type | Required | Description |
|-------|------|----------|-------------|
| fileName | string | ✅ | Original file name |
| fileSize | number | ✅ | Total file size in bytes |
| mimeType | string | ✅ | File MIME type |
| visibility | 'public' \| 'private' | ❌ | File access level (default: 'private') |
| metadata | object | ❌ | Arbitrary key-value metadata |
| totalChunks | number | ✅ | Expected number of chunks |
Response 201 Created:
{
"fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"uploadToken": "scoped-upload-token-xyz"
}| Field | Type | Description |
|-------|------|-------------|
| fileId | string | Server-assigned file identifier |
| uploadToken | string? | Optional scoped token for this upload |
POST /files/:fileId/chunks
Upload a single chunk. Called sequentially for each chunk (0 to N-1).
URL Parameters:
| Param | Type | Description |
|-------|------|-------------|
| fileId | string | File ID from the init response |
Headers:
Authorization: Bearer <token>Note: Content-Type is
multipart/form-data— set automatically by the browser.
Request body (FormData):
| Field | Type | Description |
|-------|------|-------------|
| chunk | Blob | The chunk data |
| chunkIndex | string | 0-based chunk index |
| totalChunks | string | Total number of chunks |
Response 200 OK:
{
"received": true
}POST /files/:fileId/complete
Finalise the upload after all chunks have been received.
URL Parameters:
| Param | Type | Description |
|-------|------|-------------|
| fileId | string | File ID from the init response |
Headers:
Authorization: Bearer <token>Response 200 OK:
{
"fileId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"url": "https://cdn.example.com/files/f47ac10b.pdf",
"size": 15728640,
"mimeType": "application/pdf"
}| Field | Type | Description |
|-------|------|-------------|
| fileId | string | File identifier |
| url | string | Public/signed URL to the assembled file |
| size | number | Final file size in bytes |
| mimeType | string | File MIME type |
GET /files
List files with optional filtering, sorting, and pagination.
Headers:
Authorization: Bearer <token>Query Parameters:
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| page | number | 1 | Page number (1-based) |
| limit | number | 20 | Items per page |
| userId | string | — | Filter by file owner |
| visibility | 'public' \| 'private' | — | Filter by visibility |
| mimeType | string | — | Filter by MIME (e.g. image/*) |
| sortBy | 'createdAt' \| 'size' \| 'fileName' | 'createdAt' | Sort field |
| sortOrder | 'asc' \| 'desc' | 'desc' | Sort direction |
Example: GET /files?page=1&limit=10&userId=user-123&visibility=public&sortBy=size&sortOrder=desc
Response 200 OK:
{
"data": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"fileName": "photo.jpg",
"mimeType": "image/jpeg",
"size": 2048576,
"visibility": "public",
"url": "https://cdn.example.com/files/photo.jpg",
"metadata": { "description": "Team photo" },
"createdAt": "2025-01-15T10:30:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}
],
"total": 42,
"page": 1,
"limit": 10,
"totalPages": 5
}GET /files/:fileId
Get metadata for a single file.
URL Parameters:
| Param | Type | Description |
|-------|------|-------------|
| fileId | string | File identifier |
Headers:
Authorization: Bearer <token>Response 200 OK:
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"fileName": "report.pdf",
"mimeType": "application/pdf",
"size": 15728640,
"visibility": "private",
"url": "https://cdn.example.com/files/report.pdf",
"metadata": { "department": "finance" },
"createdAt": "2025-01-15T10:30:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}GET /files/:fileId/download
Download the file content as a binary blob.
URL Parameters:
| Param | Type | Description |
|-------|------|-------------|
| fileId | string | File identifier |
Headers:
Authorization: Bearer <token>Optional Query Parameters (used by the viewer for server-side transformations):
| Param | Type | Description |
|-------|------|-------------|
| w | number | Desired width (images) |
| h | number | Desired height (images) |
| q | number | Quality 1–100 (images) |
| page | number | Specific page (PDFs) |
| stream | 'true' | Request streamable format (video/audio) |
Example: GET /files/abc123/download?w=800&h=600&q=85
Response 200 OK:
- Content-Type:
<file mime type> - Body: binary file content
DELETE /files/:fileId
Delete a file from the server.
URL Parameters:
| Param | Type | Description |
|-------|------|-------------|
| fileId | string | File identifier |
Headers:
Authorization: Bearer <token>Response 204 No Content
Express Backend Demo
A complete working Express backend is available in examples/express-backend/. It implements all the endpoints above using in-memory storage — perfect for development, testing, or as a starting point for your own backend.
Quick Start
cd examples/express-backend
npm install
npm start
# Server running on http://localhost:3000Connect the Client
import { FileManager } from '@weconjs/client-fm';
const fm = FileManager.create({
baseUrl: 'http://localhost:3000',
token: 'dev-token',
});
await fm.ready();
// Upload, download, gallery — everything works out of the box
const uploader = fm.upload(file);
const result = await uploader.start();See examples/express-backend/README.md for the full source and API details.
Types
All types are exported from the package for full TypeScript support:
import type {
// Config
ClientConfig,
ServerChunkConfig,
// Endpoints (advanced customisation)
EndpointDescriptor,
EndpointConfig,
EndpointKey,
EndpointParams,
// File
FileRecord,
FileVisibility,
UploadOptions,
UploadInitRequest,
UploadInitResponse,
UploadChunkPayload,
UploadCompleteResponse,
UploadStatus,
UploadEvents,
UploadProgressPayload,
// Gallery
GalleryQuery,
GalleryResponse,
PaginatedResponse,
// Viewer
ViewFileParams,
ViewResult,
ViewStatus,
MediaCategory,
ViewerEvents,
// Batch
BatchFileEntry,
BatchProgressPayload,
BatchFileResult,
BatchUploadEvents,
// Sharing
FilePermissionType,
ShareFileRequest,
ShareUserEntry,
ShareFileBatchRequest,
UpdatePermissionRequest,
PermissionRecord,
} from '@weconjs/client-fm';License
MIT
