Unlock seamless workflows and faster delivery with our latest releases - Join the deep dive

Add extensions to asset original filenames

By Espen Hovlandsdal

🚫 MyFile / ✅ MyFile.pdf

migrations/add-asset-extensions.ts

/* eslint-disable consistent-return, no-process-env */
import {basename, extname} from 'node:path'

import {type Asset} from 'sanity'
import {at, defineMigration, patch, type SanityDocument, set} from 'sanity/migrate'

const REPLACE_EXTENSION =
  typeof process === 'object' &&
  typeof process.env === 'object' &&
  'REPLACE_EXTENSION' in process.env

// Some formats are considered "equivalent" for our purposes, and we want to maintain them
const aliases = [
  ['jpeg', 'jpg'],
  ['tiff', 'tif'],
  ['mp4', 'mp'],
]

export default defineMigration({
  title: 'Add asset extensions',
  documentTypes: ['sanity.fileAsset', 'sanity.imageAsset'],
  migrate: {
    document(doc) {
      if (!isAsset(doc)) {
        return
      }

      // Actual extension set in `originalFilename`, normalized to lowercase and without leading dot
      const rawExtension = extname(doc.originalFilename)
      const actualExtension = extname(doc.originalFilename.toLowerCase()).replace(/^./, '')

      // The "wanted" extension, as inferred by Sanity backend
      const wantedExtension = doc.extension.toLowerCase()

      // Do we already have the wanted extension?
      const hasWantedExtension = actualExtension === wantedExtension
      if (hasWantedExtension) {
        return
      }

      // Some are acceptable as aliases, eg `tif` instead of `tiff`, `jpg` instead of `jpeg`
      const hasAliasExtension = aliases.some(
        (group) => group.includes(actualExtension) && group.includes(wantedExtension),
      )
      if (hasAliasExtension) {
        return
      }

      // Note; this may end up as `filename.some.pdf`, if the original filename was `filename.some`
      const withExtension = REPLACE_EXTENSION
        ? `${basename(doc.originalFilename, rawExtension)}.${wantedExtension}`
        : `${doc.originalFilename}.${wantedExtension}`

      return patch(doc._id, [at('originalFilename', set(withExtension))])
    },
  },
})

// Should always be true given the `documentTypes` filter, but for TypeScript safety,
// let's be overly pedantic/defensive.
function isAsset(doc: SanityDocument): doc is Asset & {originalFilename: string} {
  return (
    (doc._type === 'sanity.fileAsset' || doc._type === 'sanity.imageAsset') &&
    'extension' in doc &&
    'originalFilename' in doc
  )
}

A migration script for the sanity migration command which finds all asset documents (they are what represents your uploaded images and files) which has an "original filename" that is missing a "correct" extension.

This usually shouldn't happen, but it could if you are uploading files/images through the API or using a client and you forget to include an extension when specifying a filename parameter. Files can also be uploaded with a file type that does not match what the actual content is, such as uploading a .png file that is actually a .jpg.

Place the migration in the <studio>/migrations directory and run it like you normally would:

sanity migration run add-asset-extensions

Note that this migration script by default will add an additional extension to files that already has one - in the above PNG/JPG mismatch scenario, the file name could end up as filename.png.jpg. If you would rather replace the filename, you can set an environment variable REPLACE_EXTENSION to force that behavior:

REPLACE_EXTENSION npx sanity migration run add-asset-extensions

Contributor

Other schemas by author

Auto-reload Studio when changes are deployed

Drop this into your Studio to let editors know when there's a more recent version of your Studio available, making sure they have the latest fields and validations.

Go to Auto-reload Studio when changes are deployed