FluxMedia
Copy page
Open in ChatGPTOpen in Claude

Plugins

Extend FluxMedia with custom functionality using the plugin system.

Installation

pnpm add @fluxmedia/plugins

# For image optimization / metadata extraction:
pnpm add sharp

Using Plugins

import { MediaUploader } from '@fluxmedia/core';
import { 
  createFileValidationPlugin,
  createAnalyticsPlugin 
} from '@fluxmedia/plugins';

const uploader = new MediaUploader(provider);

// Add plugins
await uploader.use(createFileValidationPlugin({
  maxSize: 10 * 1024 * 1024,
  allowedTypes: ['image/*']
}));

await uploader.use(createAnalyticsPlugin({
  logLevel: 'info'
}));

Available Plugins

File Validation

Validates files before upload.

import { createFileValidationPlugin } from '@fluxmedia/plugins';

const plugin = createFileValidationPlugin({
  allowedTypes: ['image/*', 'video/mp4'],
  maxSize: 10 * 1024 * 1024, // 10MB
  blockedExtensions: ['.exe', '.bat'],
  useMagicBytes: true,
});

Image Optimization (Server-side)

Optimizes images using sharp before upload.

import { createImageOptimizationPlugin } from '@fluxmedia/plugins';

const plugin = createImageOptimizationPlugin({
  maxWidth: 2000,
  maxHeight: 2000,
  quality: 0.85,
  format: 'webp',
});

Metadata Extraction

Extracts EXIF data, dimensions, and file hashes. Requires sharp as a peer dependency for image metadata.

import { createMetadataExtractionPlugin } from '@fluxmedia/plugins';

const plugin = createMetadataExtractionPlugin({
  extractExif: true,        // EXIF data (orientation, camera, GPS)
  extractDimensions: true,  // Image width/height
  hashFile: true,           // Generate file hash
  hashAlgorithm: 'md5',    // 'md5' or 'sha256'
});

Extracted metadata is added to options.metadata.extracted:

interface ExtractedMetadata {
  dimensions?: { width: number; height: number };
  exif?: {
    make?: string;
    model?: string;
    orientation?: number;
    dateTime?: string;
    exposureTime?: number;
    fNumber?: number;
    iso?: number;
    focalLength?: number;
    gps?: { latitude?: number[]; longitude?: number[] };
  };
  hash?: string;
  hashAlgorithm?: string;
}

This plugin is marked as optional: true — extraction failures won't block uploads.

Analytics

Logs and tracks upload operations with strongly-typed events. Environment-aware with configurable log levels.

import { createAnalyticsPlugin, type TrackFunction } from '@fluxmedia/plugins';

const plugin = createAnalyticsPlugin({
  environment: 'production',  // 'development' | 'production' | 'test' | 'all'
  logLevel: 'info',           // 'none' | 'error' | 'warn' | 'info' | 'debug'
  trackPerformance: true,     // Track upload duration and speed
  console: true,              // Log to console
  // Typed track function — TypeScript verifies event/data match
  track: (event, data) => {
    // event: 'media.upload.started' | 'media.upload.completed' | 'media.delete.completed' | 'media.error'
    myAnalytics.track(event, data);
  },
  // Individual callbacks
  onUploadStart: (file, options) => { /* ... */ },
  onUploadComplete: (result, durationMs) => { /* ... */ },
  onUploadError: (error, file) => { /* ... */ },
  onDelete: (id) => { /* ... */ },
});

Event Types: media.upload.started, media.upload.completed, media.delete.completed, media.error

Performance metadata is automatically added to upload results when trackPerformance: true:

result.metadata.performance = {
  duration: 1234,       // ms
  uploadSpeed: 524288,  // bytes/sec
};

This plugin is marked as optional: true — analytics failures won't block uploads.

Retry

Provides automatic retry with exponential backoff. Automatically resumes multipart uploads from PartialUploadError context.

import { createRetryPlugin, withRetry } from '@fluxmedia/plugins';

// 1. Add retry plugin to enrich upload options with retry metadata
await uploader.use(createRetryPlugin({
  maxRetries: 3,
  retryDelay: 1000,
  exponentialBackoff: true,
  retryableErrors: [
    MediaErrorCode.NETWORK_ERROR,
    MediaErrorCode.RATE_LIMITED,
    MediaErrorCode.UPLOAD_FAILED,
  ],
  onRetry: (attempt, error, delay) => {
    console.log(`Retry ${attempt} after ${delay}ms`);
  },
  shouldRetry: (error, attempt) => {
    // Custom retry logic
    return attempt < 5;
  },
}));

// 2. Use withRetry for automatic retry logic with PartialUploadError resume
const result = await withRetry(
  (resumeContext) => uploader.upload(file, {
    metadata: resumeContext ? { _resumeFrom: resumeContext } : {},
  }),
  { maxRetries: 3, exponentialBackoff: true }
);

Key features:

  • Exponential backoff: delays double each retry (1s → 2s → 4s → ...)
  • PartialUploadError awareness: withRetry captures the upload context and passes it to the next attempt, enabling multipart upload resumption
  • Custom shouldRetry function for fine-grained control
  • getRetryConfig(options) to read retry metadata from upload options

Creating Custom Plugins

Using createPlugin Helper

import { createPlugin } from '@fluxmedia/core';

const myPlugin = createPlugin('my-plugin', {
  beforeUpload: async (file, options) => {
    console.log('Uploading:', file);
    return { file, options };
  },
  afterUpload: async (result) => {
    console.log('Uploaded:', result.url);
    return result;
  },
  onError: async (error, context) => {
    console.error(`Failed in ${context.phase} phase:`, error.message);
  },
}, { optional: true }); // Optional plugins degrade gracefully

Manual Plugin Definition

import type { FluxMediaPlugin } from '@fluxmedia/core';

const watermarkPlugin: FluxMediaPlugin = {
  name: 'watermark',
  version: '1.0.0',
  optional: false,  // Errors will propagate (default behavior)
  
  async init() {
    // Called when registered via uploader.use()
    console.log('Watermark plugin initialized');
  },
  
  async destroy() {
    // Called when unregistered via uploader.plugins.unregister()
    console.log('Watermark plugin destroyed');
  },
  
  hooks: {
    beforeUpload: async (file, options) => {
      // Add watermark to image...
      return { file: watermarkedFile, options };
    },
  },
};

Plugin Lifecycle

Registration & Initialization

When a plugin is registered with uploader.use():

  1. If a plugin with the same name exists, it is unregistered first (calling destroy())
  2. The new plugin's init() method is called (if defined)
  3. The plugin is stored in the plugin manager

Optional Plugins

Plugins with optional: true enable graceful degradation:

const plugin = createPlugin('non-critical', {
  afterUpload: async (result) => {
    await sendToAnalytics(result); // If this throws...
    return result;
  },
}, { optional: true }); // ...the upload still succeeds

When an optional plugin's hook throws:

  • The error is caught silently
  • The upload pipeline continues
  • Useful for analytics, logging, metadata extraction

Plugin Hooks

Hooks execute in registration order:

  • beforeUpload - Modify file/options before upload. Return { file, options } to modify, or void to pass through.
  • afterUpload - Modify result after upload. Must return UploadResult.
  • onError - Handle upload errors. Receives phase ('before', 'upload', or 'after') and optional uploadResult.
  • beforeDelete - Modify ID before deletion. Return new ID or void.
  • afterDelete - Run code after deletion.
  • beforeGetUrl - Modify URL generation parameters.

Plugin Manager API

// Check if plugin exists
uploader.plugins.has('logger');

// Get a plugin
const plugin = uploader.plugins.get('logger');

// Unregister (calls destroy())
await uploader.plugins.unregister('logger');

// Get all plugins
const allPlugins = uploader.plugins.getAll();

// Clear all (calls destroy() on each)
await uploader.plugins.clear();

Plugin Override Behavior

When registering plugins with the same name, the last one wins:

// First version
await uploader.use(createPlugin('analytics', {
  afterUpload: async (result) => {
    sendToAnalytics('v1', result);
    return result;
  },
}));

// Override with new version — previous version's destroy() is called
await uploader.use(createPlugin('analytics', {
  afterUpload: async (result) => {
    sendToAnalytics('v2', result); // This one runs
    return result;
  },
}));