How PicList's Scripting System Works: Custom JavaScript Hooks for Image Upload Workflows
PicList's scripting system allows users to execute custom JavaScript code at specific lifecycle stages (such as before upload, after upload, or when the application opens) using a secure sandbox powered by Node.js's vm module, with access to core APIs like axios, fs, and environment variables.
The PicList scripting system in the kuingsmile/piclist repository enables power users to extend the application without modifying core source code. By placing JavaScript files in designated stage folders, you can hook into critical moments of the image upload workflow and manipulate data programmatically. This architecture separates user customizations from the main application logic while maintaining full access to PicList's internal context and Node.js utilities.
Architecture and Lifecycle Stages
The scripting engine operates through a structured pipeline that resolves scripts by lifecycle stage, filters enabled configurations, and executes code within a controlled sandbox environment.
Directory Structure and Stage Resolution
Scripts reside in a physical directory structure organized by lifecycle hooks. The scriptsDir() function in src/main/utils/runScript.ts returns the root scripts folder, which contains subdirectories named after specific stages:
scripts/onSoftwareOpen/– Executes when PicList launchesscripts/beforeUpload/– Runs prior to image upload initiationscripts/upload/– Executes during the upload processscripts/afterUpload/– Runs after successful uploads complete- Special stages like
uploader.advancedplistfor advanced plugin hooks
The scriptLifecycleStages array (lines 28-41 of src/main/utils/runScript.ts) enumerates all supported hook names, allowing plugin developers to register custom stages beyond the defaults.
The Execution Pipeline
When PicList triggers a lifecycle event, the runScriptInStage function (lines 103-139 of src/main/utils/runScript.ts) orchestrates the following flow:
- Path Resolution: Constructs the absolute path
<scriptsRoot>/<stage>with special handling for advanced plugin stages - Filtering: Skips scripts listed in
config.scripts.disabledListas defined insrc/renderer/utils/configPaths.ts - Environment Preparation: The
getFreshEnv()function (lines 14-24) parses.envfiles usingdotenvand injects variables intoprocess.env - Sandbox Creation: Uses Node's
vmmodule to create an isolated context with controlled global access - Script Evaluation: Executes the raw JavaScript within the sandbox context
- Entry Point Invocation: Calls the user-defined
main(ctx, extra)async function and awaits its return value
Sandbox Environment and Exposed APIs
The runScript function (lines 54-96 of src/main/utils/runScript.ts) creates a restricted execution context using vm.createContext(). This sandbox exposes specific PicList and Node.js utilities while blocking unsafe operations.
Available Global Objects
Each script receives access to:
ctx– The PicList core context providing logging viactx.log.info(),ctx.log.error(), and configuration accessextra– Stage-specific data (e.g., the current upload payload or file metadata)env– Parsed environment variables from the.envfile loaded bygetFreshEnv()console– Wrapped to forward output to PicList's internal logger- Node.js modules:
axios,crypto,fs,path,os,Buffer, and timing functions base64Encodeandbase64Decode– Helper utilities for data transformation
The sandbox prevents access to the parent process or file system beyond the exposed APIs, ensuring that custom scripts cannot compromise application stability or access sensitive system resources outside the designated scope.
Writing Custom Scripts for PicList
Scripts must export a main function that accepts ctx and extra parameters. The function can be synchronous or asynchronous (returning a Promise).
Modifying Upload Payloads
Create a file at scripts/beforeUpload/modifyPayload.js to intercept and transform upload data:
function main(ctx, extra) {
// Log to PicList's output panel
ctx.log.info('Processing upload with custom script')
// Access upload payload via extra
const originalName = extra.fileName
// Modify the payload for downstream processing
extra.payload = {
...extra.payload,
customTimestamp: Date.now(),
originalIdentifier: originalName
}
// Return modified extra object to pass changes forward
return extra
}
Using Environment Variables
Place a .env file in your scripts root directory, then access variables in scripts/onSoftwareOpen/loadEnv.js:
function main(ctx) {
// Variables from .env are available on the global env object
const apiKey = env.API_KEY
const endpoint = env.CUSTOM_ENDPOINT
if (!apiKey) {
ctx.log.warn('API_KEY not configured in .env file')
return
}
ctx.log.info(`Configured endpoint: ${endpoint}`)
ctx.setConfig('customEndpoint', endpoint)
}
Asynchronous Operations
Scripts support async/await for network requests or file operations:
async function main(ctx, extra) {
try {
// Use the bundled axios instance
const response = await axios.get('https://api.example.com/validate', {
params: { filename: extra.fileName }
})
if (!response.data.allowed) {
ctx.log.error('Upload blocked by validation service')
throw new Error('Validation failed')
}
// Simulate processing delay
await new Promise(resolve => setTimeout(resolve, 500))
return extra
} catch (error) {
ctx.log.error(`Script execution failed: ${error.message}`)
throw error
}
}
Configuration and Script Management
Users control script execution through PicList's configuration interface and Vue-based management UI.
Disabling Scripts and Security
The configuration object picgo.getConfig().scripts (defined in src/renderer/utils/configPaths.ts) contains:
disabledList– An array of script paths (relative to the scripts directory) that the engine should ignore during stage executiongithubToken– Optional credentials for accessing the script marketplace
The ScriptPage.vue component in src/renderer/pages/ provides the graphical interface for toggling scripts on/off and editing code directly within the application.
Error Handling Behavior
When a script throws an exception or returns a rejected Promise, the error propagates through ctx.log.error and re-throws to the caller. This allows the host stage to decide whether to abort the current operation (such as canceling an upload) or continue execution. The sandbox ensures that script crashes do not destabilize the main PicList process.
Summary
- Sandboxed Execution: PicList uses Node.js's
vmmodule to run scripts in isolated contexts with controlled access toaxios,fs,crypto, and other utilities viasrc/main/utils/runScript.ts. - Lifecycle Hooks: Scripts organize into stage-specific folders (
beforeUpload,onSoftwareOpen, etc.) enumerated inscriptLifecycleStages, withrunScriptInStagedispatching execution at appropriate times. - Data Flow: Scripts receive
ctx(core PicList context) andextra(stage-specific data) parameters, modify objects in-place, and return values to influence downstream processing. - Environment Support: The
getFreshEnv()function automatically loads.envfiles into the sandbox'senvglobal variable, enabling secure credential management without hardcoding secrets. - Management Interface: Users enable, disable, and edit scripts through
ScriptPage.vue, with persistence handled viapicgo.getConfig().scripts.disabledList.
Frequently Asked Questions
What lifecycle stages are available in PicList?
PicList supports onSoftwareOpen, beforeUpload, upload, and afterUpload stages by default, with additional extensible stages like uploader.advancedplist available for plugin developers. The complete list is maintained in the scriptLifecycleStages array within src/main/utils/runScript.ts. Each stage corresponds to a subfolder in the scripts directory, and scripts placed in these folders execute automatically when that lifecycle event triggers.
How do I access environment variables in a PicList script?
Create a .env file in your root scripts directory (alongside the stage folders). When runScriptInStage executes, the getFreshEnv() function parses this file using dotenv and injects the variables into process.env while also exposing them as a global env object within the sandbox. Access variables directly via env.VARIABLE_NAME without requiring any import statements.
Is PicList's scripting system secure?
Yes, the system employs multiple security layers. The vm module creates an isolated context that prevents scripts from accessing the parent process or unrestricted file system. Only explicitly exposed APIs (such as axios, fs, ctx, and console) are available within the sandbox. Additionally, the disabledList configuration allows administrators to permanently prevent specific scripts from executing, and errors within scripts bubble up without crashing the main application.
Where does PicList store custom scripts on disk?
Scripts reside in the directory returned by the scriptsDir() function, typically located in the application's user data folder under a scripts subdirectory. The engine organizes scripts into stage-named subfolders (e.g., scripts/beforeUpload/). You can view and modify the current scripts directory path through src/main/utils/runScript.ts, and manage individual script files via the Script Page interface in src/renderer/pages/ScriptPage.vue.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →