Skip to content

lkho/payload-custom-path-local-storage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Payload Custom Path Local Storage Plugin

License: MIT Downloads


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.

Features / Capabilities

  • Supports customizing file paths stored on disk. Paths can be nested subdirectories.
  • Adds an endpoint to allow serving files that have slashes in filenames.
    • e.g. /api/media/a/nested/path/file.jpg instead of /api/media/a%2Fnested%2Fpath%2Ffile.jpg
  • Hooks to rewrite the urls and thumbnailURLs to match the above endpoint.
  • Fully compatible with the built-in image resizing and post deletion cleanup logic.
    • Payload can still clean up the files with nested paths when the parent document is deleted.
    • However, empty directories are not removed.

Limitations / Notes

  • This plugin only augments the built-in local storage. It does not play with any other custom adapters such as S3, GCS, plugin-cloud-storage etc.
    • The main purpose of this is to improve the file system performance when you have a large number of files.
    • I assume cloud storage providers don't have the same performance issues and organizing files in a flat structure is not a problem.
  • The added endpoint relies on the existing built-in static file handler. It only injects a wildcard URL pattern, modifies the req, and passes back to the original handler. It may break if Payload changes the internal static file handling logic in the future.
  • The modified filenames are not sanitized, and are not checked for existing overwrites. It will always overwrite existing files with the same path.
  • It is not possible to modify the filename but serving the file from a different url (i.e. the 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.

Installation

npm install https://github.com/lkho/payload-custom-path-local-storage.git#semver:^1.0.0
# or
yarn add ...
# or
pnpm install ...

Usage

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;
          },
        },
      },
    }),
  ],
});

Examples

Place files in date sub folders

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

Hash paths to avoid too many files in a single directory

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

Append hash to filenames for cache busting

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

Contributing

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!

About

A Payload CMS local storage plugin which allows you to specify a custom path for each stored file.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published