Skip to content

Instantly share code, notes, and snippets.

@grncdr
Last active March 3, 2023 05:57
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save grncdr/afc97da1869f5fb3aef1 to your computer and use it in GitHub Desktop.
Save grncdr/afc97da1869f5fb3aef1 to your computer and use it in GitHub Desktop.
Delete unused assets

Delete unused assets

This is an example script for deleting assets that aren't linked in your content model. It does this by walking through all assets and checking for any links back to them.

WARNING: This script does not take into account assets that are only linked inside of Text fields. If you primarily embed images directly using the markdown editor, this will very likely delete assets you depend on.

You must fill in your own CMA access token & space ID at the top before running

Usage

git clone git@gist.github.com:/afc97da1869f5fb3aef1.git cleanup-assets
cd cleanup-assets
# Edit index.js to set up your credentials
npm install && npm run cleanup-assets
#!/usr/bin/env babel-node --stage=0
import fs from 'fs';
import contentful from 'contentful-management';
import xtend from 'xtend';
const client = contentful.createClient({
// Get one by logging in at https://www.contentful.com/developers/documentation/content-management-api/
accessToken: 'CMA ACCESS TOKEN'
});
const spaceId = 'YOUR SPACE ID';
async function main () {
const space = await client.getSpace(spaceId);
const assetLinkFields = await findAssetLinkFields(space);
console.log(assetLinkFields);
await walkAssets(space, maybeDeleteAsset.bind(null, space, assetLinkFields))
}
async function findAssetLinkFields (space) {
const contentTypes = await space.getContentTypes({});
return contentTypes.reduce((assetLinkFields, contentType) => {
assetLinkFields[contentType.sys.id] = contentType.fields.filter(fieldIsAssetLink).map((field) => field.id)
return assetLinkFields;
}, {});
}
function fieldIsAssetLink (field) {
return (field.type === 'Link' && field.linkType === 'Asset') ||
(field.type === 'Array' && fieldIsAssetLink(field.items))
}
async function walkAssets (space, fn) {
const order = '-sys.createdAt'
const limit = 1000;
async function next (skip) {
const items = await space.getAssets({ skip, limit, order })
await Promise.all(items.map(fn))
if (items.length === limit) {
return next(skip + limit)
}
}
return next(0);
}
async function maybeDeleteAsset (space, assetLinkFields, asset) {
const id = asset.sys.id
var linkCount = 0;
for (var contentTypeId in assetLinkFields) {
let fieldIds = assetLinkFields[contentTypeId];
for (var i = 0, len = fieldIds.length; i < len; i++) {
let fieldId = fieldIds[i];
const entries = await space.getEntries({
content_type: contentTypeId,
[`fields.${fieldId}.sys.id`]: asset.sys.id
})
linkCount += entries.length;
}
}
if (!linkCount) {
// No links to this asset from the selected field, safe to delete
if (asset.sys.publishedVersion) {
asset = await space.unpublishAsset(asset);
}
await space.deleteAsset(asset);
}
}
main().catch((err) => {
console.error(err.stack);
process.exit(1);
});
{
"name": "cleanup-assets",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"cleanup-assets": "babel-node --stage=0 index.js"
},
"author": "Stephen Sugden",
"license": "MIT",
"dependencies": {
"babel": "^5.5.8",
"contentful-management": "^0.1.1",
"xtend": "^4.0.0"
}
}
@arminrosu
Copy link

arminrosu commented Nov 11, 2020

In case you ended up here from google like me, and want to keep linked assets, edit maybeDeleteAsset to:

async function maybeDeleteAsset (space, assetLinkFields, asset) {
  const id = asset.sys.id;
  const referencesToAsset = await space.getEntries({
    links_to_asset: id
  })

  if (!referencesToAsset.length > 0) {
    console.log('DELETE', id, asset.fields.title)
    // No links to this asset from the selected field, safe to delete
    if (asset.sys.publishedVersion) {
      asset = await space.unpublishAsset(asset);
    }
    await space.deleteAsset(asset);
  } else {
    console.log('KEEP', id, asset.fields.title)
  }
}

@grncdr thanks a bunch for the snippet!

@grncdr
Copy link
Author

grncdr commented Nov 14, 2020

I would be surprised if Contentful hasn’t implemented a better way to accomplish this task in the last 5 years, but I’m glad it helped. (I’m very surprised this still works at all tbh 😅)

@arminrosu
Copy link

@ grncdr nope, it's basically the same. To their credit, the contentful-management@0.11 still works today! Upgrading to @6, you only need to add a call to getEnvironment, pass that along instead of space, getEntries doesn't return an array of entries but { items: entries[] }. 5 years of backwards compatibility, amazing!

@imshuffling
Copy link

@arminrosu do you have an example snippet using getEnvironment? :)

@arminrosu
Copy link

arminrosu commented Mar 24, 2021

@imshuffling sorry, I don't have a 1:1 update of the code above. But basically you need to use environment instead of space. Most of the methods are the same.

@samuel99
Copy link

samuel99 commented Feb 24, 2023

If anyone else comes across this from Google in 2023, this version works as of 24th of February:

import contentful from "contentful-management";
import { config } from "./config.js";

const client = contentful.createClient({
  accessToken: config.MANAGEMENT_TOKEN,
});

const spaceId = config.SPACE_ID;

async function main() {
  const environment = await (
    await client.getSpace(spaceId)
  ).getEnvironment(config.ENVIRONMENT);

  const assetLinkFields = await findAssetLinkFields(environment);
  await walkAssets(
    environment,
    maybeDeleteAsset.bind(null, environment, assetLinkFields)
  );
}

async function findAssetLinkFields(environment) {
  const contentTypes = await environment.getContentTypes({});
  return contentTypes.items.reduce((assetLinkFields, contentType) => {
    assetLinkFields[contentType.sys.id] = contentType.fields
      .filter(fieldIsAssetLink)
      .map((field) => field.id);
    return assetLinkFields;
  }, {});
}

function fieldIsAssetLink(field) {
  return (
    (field.type === "Link" && field.linkType === "Asset") ||
    (field.type === "Array" && fieldIsAssetLink(field.items))
  );
}

async function walkAssets(environment, fn) {
  const order = "-sys.createdAt";
  const limit = 10;

  async function next(skip) {
    const assets = await environment.getAssets({ skip, limit, order });
    await Promise.all(assets.items.map(fn));
    if (assets.items.length === limit) {
      return next(skip + limit);
    }
  }

  return next(0);
}

async function maybeDeleteAsset(environment, assetLinkFields, asset) {
  const id = asset.sys.id;
  const referencesToAsset = await environment.getEntries({
    links_to_asset: id,
  });
  //console.log(asset.sys.id);
  if (referencesToAsset.total > 0) {
    //console.log("KEEP", id, asset.fields.title);
  } else {
    if (asset.isPublished()) {
      await asset.unpublish();
      console.log("DELETE WAS PUBLISHED", id, asset.fields.title);
    } else {
      console.log("DELETE NOT PUBLISHED", id, asset.fields.title);
    }
    await asset.delete();
  }
}
main().catch((err) => {
  console.error(err.stack);
  process.exit(1);
});

@corymcdaniel
Copy link

@samuel99 Thank you for this update and to all the others here. This has helped tremendously!

@samuel99
Copy link

samuel99 commented Mar 3, 2023

@samuel99 Thank you for this update and to all the others here. This has helped tremendously!

Np :) However I should add, that you probably have to run this script multiple times to delete all unlinked assets.

And I think this is the reason:
tmp_524abf68-9f2f-4061-a92d-da464a3a985c

(red squares are unlinked assets that are to be deleted, and green squares are assets that should be kept)

So there's room for improvement if someone wants to give it a go :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment