Skip to main content

initDynamicResourceMiddleware(options)

The initDynamicResourceMiddleware is a powerful Express middleware designed to serve files from one or more specified directories dynamically. Unlike initStaticResourceMiddleware, it actively watches for file system changes (additions, deletions, modifications) in real-time, making it ideal for serving content that can change during runtime, such as user-uploaded files, themes, or plugins.

It builds an in-memory map of resources for fast lookups and handles caching, file filtering, and conflict resolution gracefully.

How It Works

The middleware follows a clear lifecycle: initialization, scanning, watching, and serving. When a request comes in, it performs a quick lookup in its internal map. If a file is added or removed from a watched directory, the map is updated automatically.

Basic Usage

Here's how to configure the middleware to serve images from a dynamic uploads directory.

Server Setup

javascript
import express from 'express';
import { initDynamicResourceMiddleware } from '@blocklet/uploader-server';
import path from 'path';

const app = express();

const dynamicResourceMiddleware = initDynamicResourceMiddleware({
  resourcePaths: [
    {
      path: path.join(__dirname, 'uploads/images'),
      whitelist: ['.jpg', '.jpeg', '.png', '.gif'],
    },
  ],
  onReady: (count) => {
    console.log(`${count} dynamic resources are ready to be served.`);
  },
  onFileChange: (filePath, event) => {
    console.log(`File ${filePath} was ${event}.`);
  },
});

// Mount the middleware
app.use('/uploads/images', dynamicResourceMiddleware);

// On server shutdown, clean up watchers
process.on('SIGINT', () => {
  if (dynamicResourceMiddleware.cleanup) {
    dynamicResourceMiddleware.cleanup();
  }
  process.exit();
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

Configuration Options

The initDynamicResourceMiddleware function accepts a single options object with the following properties:

OptionTypeDescription
componentDidstringOptional. If provided, the middleware will only activate if the current component's DID matches this value.
resourcePathsDynamicResourcePath[]Required. An array of objects defining the directories to watch and serve. See details below.
watchOptionsobjectOptional. Configuration for the file system watcher.
cacheOptionsobjectOptional. Configuration for HTTP caching headers.
onFileChange(filePath: string, event: string) => voidOptional. A callback function that triggers when a file is changed, added, or deleted. The event can be 'change', 'rename', or 'delete'.
onReady(resourceCount: number) => voidOptional. A callback that runs after the initial scan is complete and when the resource map changes, providing the total count of available resources.
setHeaders(res, filePath, stat) => voidOptional. A function to set custom headers on the response before serving a file.
conflictResolution'first-match' | 'last-match' | 'error'Optional. Strategy to handle filename collisions when multiple directories contain a file with the same name. Defaults to 'first-match'.

DynamicResourcePath Object

Each object in the resourcePaths array defines a source for dynamic assets.

PropertyTypeDescription
pathstringRequired. The absolute path to the directory. It supports glob patterns (e.g., /path/to/plugins/*/assets) to watch multiple matching directories.
whiteliststring[]Optional. An array of file extensions (e.g., ['.png', '.svg']) to include. If specified, only files with these extensions will be served.
blackliststring[]Optional. An array of file extensions to exclude.

watchOptions Object

PropertyTypeDescription
ignorePatternsstring[]An array of string patterns or regular expressions to ignore during watching.
persistentbooleanIf true, the process will continue running as long as files are being watched. Defaults to true.
usePollingbooleanWhether to use polling for watching files. Can be necessary for certain network file systems.
depthnumberThe depth of subdirectories to watch. If undefined, it watches recursively.

cacheOptions Object

PropertyTypeDescription
maxAgestring | numberSets the Cache-Control max-age header. Can be a number in milliseconds or a string like '365d'. Defaults to '365d'.
immutablebooleanIf true, adds the immutable directive to the Cache-Control header. Defaults to true.
etagbooleanWhether to enable ETag generation.
lastModifiedbooleanWhether to enable the Last-Modified header.

Advanced Usage

Using Glob Patterns

To serve assets from multiple plugin directories, you can use a glob pattern. The middleware will find all matching directories and watch them for changes.

Glob Pattern Example

javascript
const middleware = initDynamicResourceMiddleware({
  resourcePaths: [
    {
      // Watch the 'assets' folder inside every directory under 'plugins'
      path: path.join(__dirname, 'plugins', '*', 'assets'),
      whitelist: ['.css', '.js', '.png'],
    },
  ],
});

Conflict Resolution

If two watched directories contain a file named logo.png, the conflictResolution strategy determines which one is served:

  • 'first-match' (default): The first one found during the initial scan is used. Subsequent finds are ignored.
  • 'last-match': The last one found will overwrite any previous entry. This is useful if you have an override mechanism.
  • 'error': Logs an error to the console indicating a conflict, and typically the first-match behavior is used.

Return Value

The initDynamicResourceMiddleware function returns an Express middleware function. This returned function also has a cleanup method attached to it.

cleanup()

This method should be called during a graceful server shutdown. It stops all file system watchers and clears the internal resource maps to prevent memory leaks and release file handles.

Cleanup Example

javascript
const server = app.listen(3000);
const dynamicMiddleware = initDynamicResourceMiddleware(/* ...options */);

// ...

function gracefulShutdown() {
  console.log('Shutting down server...');
  if (dynamicMiddleware.cleanup) {
    dynamicMiddleware.cleanup();
  }
  server.close(() => {
    console.log('Server closed.');
    process.exit(0);
  });
}

process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);