How to Create a Custom Uploader Plugin in PicList-Core: A Complete Developer's Guide

To create a custom uploader plugin in PicList-Core, you must export a register function that calls ctx.helper.uploader.register() with your uploader's name, an async handle function that iterates over ctx.output to upload images and populate img.imgUrl, and a config function that returns an array of IPluginConfig objects defining the settings UI.

PicList-Core is the extensible engine behind the PicList image upload tool, designed to support both built-in and third-party upload adapters. By implementing the uploader plugin contract defined in the kuingsmile/piclist-core repository, you can add support for any image hosting service—whether deployed as an internal module or distributed via npm.

Understanding the Plugin Architecture

PicList-Core loads uploader plugins through a unified plugin system that supports two distribution methods. Both follow identical implementation contracts but differ in how they are discovered and loaded by the core.

Built-in vs External Plugins

Built-in plugins reside directly within the repository under src/plugins/uploader/ and are registered in src/plugins/uploader/index.ts. These ship with PicList-Core and require no separate installation.

External plugins are published as npm packages matching the pattern picgo-plugin-* (or scoped @*/picgo-plugin-*). The PluginLoader class in src/lib/PluginLoader.ts automatically discovers these packages at runtime using PluginLoader.getPlugin and PluginLoader.registerPlugin.

The Uploader Contract

Every uploader must provide three core components to the registration helper:

  • handle – An async function receiving the IPicGo context that uploads images and mutates ctx.output in place
  • config – A function returning IPluginConfig[] that defines user-configurable fields in the settings UI
  • name – A display string identifying the uploader in the interface

According to the reference implementation in src/plugins/uploader/local.ts, the register function acts as the entry point called by PicList-Core during initialization.

Implementing a Custom Uploader

Follow these steps to create a functional uploader plugin, whether built-in or external.

Step 1: Create the Plugin File

Create a new TypeScript file in src/plugins/uploader/ (for built-in) or as your package entry point (for external). The file must export a default register function that receives the IPicGo context.

import { IPicGo, IPluginConfig } from '../../types'
import { createField, encodePath } from './utils'

export default function register(ctx: IPicGo): void {
  ctx.helper.uploader.register('myUploader', {
    get name() { return 'My Custom Uploader' },
    handle,
    config,
  })
}

Step 2: Implement the Handle Function

The handle function processes the upload queue available in ctx.output. For each image object, you must set img.imgUrl to the public URL and optionally img.hash and img.galleryPath.

Based on the implementation in src/plugins/uploader/local.ts, the function should:

  1. Retrieve configuration via ctx.getConfig('picBed.myUploader')
  2. Iterate over ctx.output to access image buffers
  3. Upload data via HTTP request or SDK
  4. Populate required fields and clean up temporary buffers
async function handle(ctx: IPicGo): Promise<IPicGo> {
  const cfg = ctx.getConfig<any>('picBed.myUploader')
  if (!cfg?.apiUrl) throw new Error('Missing API URL in myUploader config')

  for (const img of ctx.output) {
    if (!img.fileName) continue
    const data = img.buffer ?? (img.base64Image ? Buffer.from(img.base64Image, 'base64') : undefined)
    if (!data) continue

    // Upload to your service
    const resp = await fetch(cfg.apiUrl, {
      method: 'POST',
      headers: { Authorization: `Bearer ${cfg.token}` },
      body: data,
    })
    if (!resp.ok) throw new Error(`Upload failed: ${resp.statusText}`)
    const result = await resp.json()

    img.imgUrl = encodePath(result.url)
    img.hash = result.url
    img.galleryPath = result.url
    delete img.base64Image
    delete img.buffer
  }
  return ctx
}

Step 3: Define the Configuration Interface

The config function returns an array defining the settings UI. Use createField from src/plugins/uploader/utils.ts to maintain consistency with built-in uploaders.

function config(ctx: IPicGo): IPluginConfig[] {
  const userCfg = ctx.getConfig<any>('picBed.myUploader') ?? {}
  return [
    createField(ctx, 'myUploader', 'apiUrl', 'input', userCfg.apiUrl || '', true),
    createField(ctx, 'myUploader', 'token', 'input', userCfg.token || '', true),
  ]
}

Step 4: Register the Plugin

For built-in plugins, add the registration call to src/plugins/uploader/index.ts:

import myUploader from './myUploader'

const buildInUploaders = () => {
  return {
    register(ctx: IPicGo) {
      // ... existing built-ins ...
      myUploader(ctx)
    },
  }
}

For external plugins, ensure your package.json follows the naming convention:

{
  "name": "picgo-plugin-my-uploader",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "author": "Your Name"
}

Plugin Discovery and Loading Mechanics

The PluginLoader class orchestrates how PicList-Core finds and initializes plugins. When the application starts, PluginLoader.load() scans for enabled third-party packages, while src/plugins/uploader/index.ts aggregates built-in registrations.

During the upload execution flow, src/lib/Commander.ts invokes the registered uploader's handle method. The uploader helper (ctx.helper.uploader) stores all registered adapters and routes requests based on the user's selected picBed configuration.

Key utility functions centralized in src/plugins/uploader/utils.ts include:

  • createField – Generates standardized configuration UI components
  • encodePath – URL-encodes file paths for safe linking
  • formatPathHelper – Handles path formatting across operating systems

Summary

  • Plugin Contract: Implement register, handle, and config functions following the pattern in src/plugins/uploader/local.ts
  • Core Requirement: The handle function must populate img.imgUrl on every item in ctx.output to complete the upload chain
  • Built-in Path: Place files in src/plugins/uploader/ and register them in src/plugins/uploader/index.ts for native inclusion
  • External Path: Publish npm packages named picgo-plugin-* to enable automatic discovery by src/lib/PluginLoader.ts
  • Utilities: Leverage createField and encodePath from src/plugins/uploader/utils.ts for consistent configuration handling

Frequently Asked Questions

What is the difference between a built-in and external uploader plugin in PicList-Core?

Built-in plugins are stored directly in the repository under src/plugins/uploader/ and compiled with the core, while external plugins are installed as npm packages matching the picgo-plugin-* naming pattern. Both implement the same register function contract, but external plugins are discovered at runtime by PluginLoader.getPlugin in src/lib/PluginLoader.ts, whereas built-ins are explicitly imported in src/plugins/uploader/index.ts.

Which fields must be set on the image object during the upload process?

According to the handle function implementation in src/plugins/uploader/local.ts, every image object in ctx.output must have img.imgUrl set to the publicly accessible URL. You should also set img.hash to a unique identifier (often the file path or URL) and optionally img.galleryPath for gallery preview support. Failure to set img.imgUrl will result in broken links in the final output.

How does PicList-Core discover external uploader plugins?

The PluginLoader class in src/lib/PluginLoader.ts scans for packages matching the regular expression /^picgo-plugin-/ or scoped patterns like @scope/picgo-plugin-name. When PluginLoader.load() executes, it resolves these packages from node_modules and calls their exported register function via PluginLoader.registerPlugin, integrating them into the ctx.helper.uploader registry.

Can I reuse existing helper functions when building a custom uploader?

Yes. PicList-Core provides centralized utilities in src/plugins/uploader/utils.ts that you should import into your plugin. The createField function generates standardized IPluginConfig objects for the settings UI, while encodePath ensures URL-safe encoding of file paths. Using these helpers maintains consistency with built-in uploaders like the local uploader defined in src/plugins/uploader/local.ts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →