npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@weconjs/client-fm

v0.1.2

Published

Browser-side file manager client with chunked uploads, caching, gallery, and headless file viewer

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

npm / yarn

yarn add @weconjs/client-fm
# or
npm install @weconjs/client-fm

CDN (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–100

4. 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 cache

20. 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=size

Custom 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/json

Request 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:3000

Connect 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