Augments Payload's built-in local file storage so uploaded files can be saved under custom, nested paths. A per-collection generateFilePath function rewrites the filename for the main file and each resized image. The plugin adds an endpoint that serves files with slashes in their names and rewrites URLs and thumbnail URLs to match. It targets Payload v3 and does not work with cloud storage adapters.
A Payload CMS local storage plugin which allows you to specify a custom path for stored files.
Installation
npm i @lkho/payload-custom-path-local-storage4
Stars
1
Forks
1
Open Issues
1 years ago
Last Updated
0
Weekly Downloads
v
npm Version
Dependencies
Trends appear after 7 days of data. Daily snapshots have not started yet.
Trends appear after 7 days of data. Daily snapshots have not started yet.
Each point is npm's rolling 7-day download count.
Health Score
Composite score based on GitHub activity, stars, npm downloads, publish recency, dependency count, and package size.
Badge for your README
[](https://payloaddirectory.dev/plugins/1074214439-root)<a href="https://payloaddirectory.dev/plugins/1074214439-root"><img src="https://payloaddirectory.dev/badge/1074214439-root.svg" alt="Payload Directory"></a>README
A plugin for Payload CMS which allows you to customize the local file paths when storing uploaded files. It is aimed at augmenting the default local storage capabilities of Payload CMS.
This plugin is developed and tested against Payload v3.
/api/media/a/nested/path/file.jpg instead of /api/media/a%2Fnested%2Fpath%2Ffile.jpgreq, and passes back to the original
handler. It may break if Payload changes the internal static file handling logic in the future.filenames are not sanitized, and are not checked for existing overwrites.
It will always overwrite existing files with the same path.filename and the url path must match.)
Since this is what the built-in static handler expects, and the file deletion logic
also relies on the filename field to locate the file on disk.npm install https://github.com/lkho/payload-custom-path-local-storage.git#semver:^1.0.0
# or
yarn add ...
# or
pnpm install ...
payload.config.ts
import { buildConfig } from 'payload/config';
import { customPathLocalStorage } from '@lkho/payload-custom-path-local-storage';
export default buildConfig({
// ...
collections: [
{
slug: 'media',
upload: {
staticDir: './media',
adminThumbnail: 'thumbnail',
imageSizes: [
{
name: 'thumbnail',
width: 100,
height: 100,
},
],
},
},
],
plugins: [
customPathLocalStorage({
collections: {
media: {
// Optional: defaults to true
addFilePathEndpoint: true,
// Optional: omit to keep the original filenames
generateFilePath: ({ data, file, payloadUploadSizes }) => {
// modify the main file path
data.filename = '...';
if (data.sizes) {
for (const v of Object.values(data.sizes)) {
v.filename = '...';
}
}
return data;
},
},
},
}),
],
});
function generateFilePath({ data }) {
const dir = new Date().toISOString().split('T')[0].replace(/-/g, '/'); // e.g. 2023/10/05
data.filename = `${dir}/${data.filename}`;
if (data.sizes) {
for (const v of Object.values(data.sizes)) {
v.filename = `${dir}/${v.filename}`;
}
}
return data;
}
Will produce:
media/2025/
media/2025/10/
media/2025/10/05/
media/2025/10/05/foo.jpg
media/2025/10/05/foo-100x100.jpg
import { createHash } from 'crypto';
function generateFilePath({ data, file }) {
// use the main file's content to generate the hash
const hash = createHash('sha256').update(file.data).digest('hex');
const dir = `${hash.slice(0, 2)}/${hash.slice(2, 4)}`;
data.filename = `${dir}/${data.filename}`;
if (data.sizes) {
for (const v of Object.values(data.sizes)) {
v.filename = `${dir}/${v.filename}`;
}
}
}
Will produce:
media/ab/
media/ab/cd/
media/ab/cd/foo.jpg
media/ab/cd/foo-100x100.jpg
import { createHash } from 'crypto';
import path from 'path';
function generateFilePath({ data, file, payloadUploadSizes }) {
const hash = (buf: Uint8Array) => {
return createHash('sha256').update(buf).digest('hex').slice(0, 4);
};
const mainFileHash = hash(file.data);
const dir = mainFileHash.slice(0, 2) + '/' + mainFileHash.slice(2, 4);
// append hash before the file extension
const parsed = path.parse(data.filename!);
data.filename = `${dir}/${parsed.name}.${mainFileHash}${parsed.ext}`;
if (data.sizes) {
for (const [k, v] of Object.entries(data.sizes)) {
if (payloadUploadSizes?.[k]) {
const parsed = path.parse(v.filename!);
v.filename = `${dir}/${parsed.name}.${hash(payloadUploadSizes[k])}${parsed.ext}`;
} else {
v.filename = `${dir}/${v.filename}`;
}
}
}
return data;
}
Will produce:
media/00/
media/00/1a/
media/00/1a/foo.001a.jpg
media/00/1a/foo-100x100.9c4b.jpg
media/48/
media/48/fa/
media/48/fa/bar.48fa.jpg
media/48/fa/bar-100x100.67d0.jpg
This plugin is developed for my personal projects and is in early development. I may not have the time to response promptly, but I welcome contributions and feedback. Feel free to open issues or submit pull requests. Any contributions are welcome!
image search
Search and import images from Unsplash, Pexels and Pixabay without leaving the Payload admin
78533/wk114v3
payload plugin aspect preview
Crop and focal point editing with live multi-aspect-ratio previews for Payload upload collections
7534/wk6v3
blur data urls
Generates blur placeholder data URLs for images uploaded to Payload media collections
6966/wk83v3
payload media
Search Unsplash, Pexels and Pixabay stock images and videos from Payload upload collections
680/wk8v4
images
On-demand Sharp image transforms with focal points, built-in blur placeholders and a favicons collection
570/wk37v3
payload icons
SVG icon upload collection with automatic optimization and a server component to render icons inline
560/wk18v3