Blobs

Blobs (Binary Large Objects) — uploaded files, images, documents — are stored through a pluggable backend system. A central registry maps backend names to handler objects, and a configuration value selects which backend is active at runtime.

The built-in backends are file (local filesystem), db (PostgreSQL), and s3 (S3-compatible object storage). You can add custom backends by implementing the BlobHandler contract and registering them.

BlobHandler contract

Each backend must be an object with four methods:

type BlobHandler = {
  read: (uuid: string, trx: Knex.Transaction) => Promise<Buffer>;
  store: (uuid: string, data: Buffer, trx?: Knex.Transaction) => Promise<void>;
  remove: (uuid: string, trx: Knex.Transaction) => Promise<void>;
  copy: (
    source: string,
    target: string,
    trx?: Knex.Transaction,
  ) => Promise<void>;
};
Method Description
read Return the blob data as a Buffer by UUID.
store Persist data under the given UUID.
remove Delete the blob identified by UUID.
copy Duplicate a blob from source UUID to target UUID.

The trx parameter is a Knex transaction. Backends that do not participate in database transactions (e.g. filesystem, S3) should accept but ignore it (convention: name the parameter _trx).

Creating a backend

Create a file that exports a BlobHandler object. Here is a minimal example that stores blobs in-memory (useful for testing):

// src/blobs/memory/memory.ts
import type { Knex } from 'knex';

const store = new Map<string, Buffer>();

export const memory = {
  read: async (uuid: string, _trx: Knex.Transaction): Promise<Buffer> => {
    const data = store.get(uuid);
    if (!data) throw `Blob not found: ${uuid}`;
    return data;
  },

  store: async (
    uuid: string,
    data: Buffer,
    _trx?: Knex.Transaction,
  ): Promise<void> => {
    store.set(uuid, data);
  },

  remove: async (uuid: string, _trx: Knex.Transaction): Promise<void> => {
    store.delete(uuid);
  },

  copy: async (
    source: string,
    target: string,
    _trx?: Knex.Transaction,
  ): Promise<void> => {
    const data = store.get(source);
    if (!data) throw `Blob not found: ${source}`;
    store.set(target, Buffer.from(data));
  },
};

Registration

In your profile’s init() function, import the blobs registry and your handler, then register it with a name:

import blobs from '@plone/nick/src/blobs';
import { memory } from '../../blobs/memory/memory';

// In init():
blobs.register('memory', memory);

Selection

The active backend is chosen at runtime by the value of config.settings.blobs. Set it in your project’s config.ts:

export const config = {
  blobs: 'memory',
  // ...
};

At startup, all registered backends are available. The facade in src/helpers/fs/fs.ts resolves the active one:

config.settings.blobs ──> blobs.get(name) ──> handler.read / store / remove / copy

Built-in implementations

Reference implementations live under src/blobs/:

Name File Storage
file src/blobs/file/file.ts Filesystem (blobsDir/)
db src/blobs/db/db.ts PostgreSQL file table
s3 src/blobs/s3/s3.ts S3-compatible object store

See the admin guide for configuration details of each backend.


This site uses Just the Docs, a documentation theme for Jekyll.