Skip to content

Commit 513ae6a

Browse files
committed
feat: add find-unused-assets script
Based on Christian Zoppi's clone-assets. The script gets all the stories and then alls assets from them. It runs a request for every asset to check if it is used in any story.
1 parent 6ca8709 commit 513ae6a

File tree

6 files changed

+238
-1
lines changed

6 files changed

+238
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,4 @@ dist
102102

103103
# TernJS port file
104104
.tern-port
105+
yarn.lock

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ Each example contains a `README.md` with an explanation about the tool.
2525
|:-------|:------:|
2626
| **[Clean Up Field Script ](https://github.com/storyblok/tool-examples/tree/main/clean-up-field)** <br/> A tool to remove a specific field from all stories | [Alexander Feiglstorfer](https://github.com/onefriendaday) |
2727
| **[Clone Assets ](https://github.com/storyblok/tool-examples/tree/main/clone-assets)** <br/> A tool to clone assets from a space to its clone | [Christian Zoppi](https://github.com/christianzoppi) |
28+
| **[Find Unused Assets ](https://github.com/storyblok/tool-examples/tree/main/find-unused-assets)** <br/> A tool to find all unused assets in a space | [Bogdan Selenginskiy](https://github.com/bseleng) |
29+
| **[](https://github.com/storyblok/tool-examples/tree/main/private-assets-demo)** <br/> undefined | undefined |
2830
| **[Storyblok Assets Backup ](https://github.com/storyblok/tool-examples/tree/main/storyblok-assets-backup)** <br/> Tool for differential backups of the assets of any Storyblok space | [Christian Zoppi](https://github.com/christianzoppi), [Gerrit Plehn](https://github.com/GerritPlehn) |
29-
| **[Private assets demo ](https://github.com/storyblok/tool-examples/tree/main/private-assets-demo)** <br/> A demo to showcase how to use private assets as gated content | [Edoardo Sandon](https://github.com/edo-san) |
3031

3132
<!-- AUTO-GENERATED-CONTENT:END -->
3233

find-unused-assets/index.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Migration from './src/index.js'
2+
import inquirer from 'inquirer'
3+
4+
const questions = [
5+
{
6+
type: 'input',
7+
name: 'oauth',
8+
message: "Please enter your Personal Access Token (get one at http://app.storyblok.com/#!/me/account)",
9+
},
10+
{
11+
type: 'input',
12+
name: 'target_space_id',
13+
message: "Please enter the Target Space Id",
14+
},
15+
{
16+
type: 'input',
17+
name: 'simultaneous_uploads',
18+
message: "Simultaneous Uploads",
19+
default: 20
20+
},
21+
{
22+
type: 'input',
23+
name: 'region',
24+
message: "Please enter the Region code. Leave empty for default EU region",
25+
default: null
26+
},
27+
]
28+
29+
inquirer.prompt(questions).then((answers) => {
30+
const migration = new Migration(answers.oauth, answers.target_space_id, answers.simultaneous_uploads, answers.region)
31+
migration.start()
32+
})

find-unused-assets/package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "find-unused-assets",
3+
"version": "0.1.1",
4+
"description": "Get all assets from a Storyblok space and run individual requests for references of those assets in stories, then print out unused assets.",
5+
"main": "index.js",
6+
"type": "module",
7+
"engines": {
8+
"node": ">=16"
9+
},
10+
"scripts": {
11+
"start": "node index"
12+
},
13+
"author": "Christian Zoppi",
14+
"contributors": [
15+
{
16+
"name": "Bogdan Selenginskiy",
17+
"email": "[email protected]"
18+
}
19+
],
20+
"license": "ISC",
21+
"dependencies": {
22+
"async": "^3.2.0",
23+
"chalk": "^4.1.0",
24+
"form-data": "^3.0.0",
25+
"inquirer": "^7.3.3",
26+
"storyblok-js-client": "^5.12.1"
27+
}
28+
}

find-unused-assets/readme.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Find Unused Assets
2+
3+
This script can be used to get names of all unused assets in a Storyblok space. It gets all assests from all stories and runs individual requests for each asset and prints its name if it is not used in any story.
4+
5+
Name | Description | Author
6+
------------ | ------------- | -------------
7+
Find Unused Assets | A tool to find all unused assets in a space | [Bogdan Selenginskiy](https://github.com/bseleng)
8+
9+
10+
## How to use
11+
12+
Run `npm i` to install and then `npm run start`.
13+
14+
You'll have to provide a Personal Access Token from your account, the id of the target space and the number of the max simultaneous uploads. The default for the max simultaneous uploads is 20 but you can increase it slightly to make the upload faster if your computer and connection can handle it or you can decrease it if you want to use less bandwidth and memory.

find-unused-assets/src/index.js

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import chalk from 'chalk'
2+
import StoryblokClient from 'storyblok-js-client'
3+
4+
// Throttling
5+
export default class Migration {
6+
constructor(oauth, target_space_id, simultaneous_uploads, region) {
7+
this.target_space_id = target_space_id
8+
this.oauth = oauth
9+
this.simultaneous_uploads = simultaneous_uploads
10+
this.region = region
11+
this.assets_retries = {}
12+
this.retries_limit = 4
13+
}
14+
15+
/**
16+
* Migration error callback
17+
*/
18+
migrationError(err) {
19+
throw new Error(err)
20+
}
21+
22+
/**
23+
* Print a message of the current step
24+
*/
25+
stepMessage(index, text, append_text) {
26+
process.stdout.clearLine()
27+
process.stdout.cursorTo(0)
28+
process.stdout.write(`${chalk.white.bgBlue(` ${index}/3 `)} ${text} ${append_text ? chalk.black.bgYellow(` ${append_text} `) : ''}`)
29+
}
30+
31+
/**
32+
* Print a message of the completed step
33+
*/
34+
stepMessageEnd(index, text) {
35+
process.stdout.clearLine()
36+
process.stdout.cursorTo(0)
37+
process.stdout.write(`${chalk.black.bgGreen(` ${index}/3 `)} ${text}\n`)
38+
}
39+
40+
/**
41+
* Start the migration
42+
*/
43+
async start() {
44+
try {
45+
await this.getTargetSpaceToken()
46+
await this.getAssets()
47+
await this.logUnusedAssets()
48+
} catch (err) {
49+
console.log(`${chalk.white.bgRed(` ⚠ Migration Error `)} ${chalk.red(err.toString().replace('Error: ', ''))}`)
50+
}
51+
}
52+
53+
/**
54+
* Get the target space token and setup the Storyblok js client
55+
*/
56+
async getTargetSpaceToken() {
57+
try {
58+
this.storyblok = new StoryblokClient({
59+
oauthToken: this.oauth,
60+
region: this.region
61+
})
62+
const space_request = await this.storyblok.get(`spaces/${this.target_space_id}`)
63+
this.target_space_token = space_request.data.space.first_token
64+
this.storyblok = new StoryblokClient({
65+
accessToken: this.target_space_token,
66+
region: this.region,
67+
oauthToken: this.oauth,
68+
rateLimit: 3
69+
})
70+
this.stepMessageEnd('1', `Personal access token is valid. New StoryblokClient is created.`)
71+
72+
} catch (err) {
73+
this.migrationError('Error trying to retrieve the space token. Please double check the target space id and the OAUTH token.')
74+
}
75+
}
76+
77+
78+
/**
79+
* Get the Assets list from the target space
80+
*/
81+
async getAssets() {
82+
this.stepMessage('2', `Fetching assets from target space.`)
83+
try {
84+
const assets_page_request = await this.storyblok.get(`spaces/${this.target_space_id}/assets`, {
85+
per_page: 100,
86+
page: 1
87+
})
88+
const pages_total = Math.ceil(assets_page_request.headers.total / 100)
89+
const assets_requests = []
90+
for (let i = 1; i <= pages_total; i++) {
91+
assets_requests.push(
92+
this.storyblok.get(`spaces/${this.target_space_id}/assets`, {
93+
per_page: 100,
94+
page: i
95+
})
96+
)
97+
}
98+
const assets_responses = await Promise.all(assets_requests)
99+
100+
this.assets_list = assets_responses.map(r => r.data.assets).flat().map((asset) => asset.filename)
101+
this.stepMessageEnd('2', `Fetched assets from target space.`)
102+
} catch (err) {
103+
this.migrationError('Error fetching the assets. Please double check the target space id.')
104+
}
105+
}
106+
107+
/**
108+
* Log unused assets
109+
*/
110+
111+
async logUnusedAssets() {
112+
this.stepMessage('3', `Log assets, which are not used in any story.`)
113+
114+
try {
115+
const unusedAssets = this.assets_list.map(async (assetFileName, i) => {
116+
// get end of URL starting with space ID
117+
//eg. "245445/901x593/ec5855f2b5/aff-lp-hero-img.png"
118+
const refSearchStart = assetFileName.indexOf(this.target_space_id)
119+
120+
// get full search string
121+
// eg. "/f/245445/901x593/ec5855f2b5/aff-lp-hero-img.png"
122+
const refSearch = assetFileName.substring(refSearchStart - 3)
123+
124+
const assetReferencesRequest = this.storyblok.get(`spaces/${this.target_space_id}/stories`, {
125+
page: 1,
126+
per_page: 25,
127+
reference_search: refSearch,
128+
})
129+
130+
return await Promise.resolve(assetReferencesRequest).then((res) => {
131+
this.stepMessage('3', ``, `${i} of ${this.assets_list.length} assets checked`)
132+
if (res.total === 0) {
133+
const lastSlashIndex = assetFileName.lastIndexOf("/")
134+
const assetClearName = assetFileName.substring(lastSlashIndex + 1)
135+
return assetClearName
136+
}
137+
})
138+
139+
140+
})
141+
142+
const unusedAssetsNames = await Promise.all(unusedAssets)
143+
let number = 1
144+
unusedAssetsNames.map((unusedAssetName, i) => {
145+
if (unusedAssetName) {
146+
process.stdout.clearLine()
147+
process.stdout.cursorTo(0)
148+
process.stdout.write(`${chalk.dim(` ${number}.`)} ${unusedAssetName}\n`)
149+
number++
150+
}
151+
})
152+
153+
this.stepMessageEnd('3', `${chalk.blueBright(unusedAssetsNames.length)} assets are checked for references in space ${chalk.dim("#" + this.target_space_id)}. You can see the list of unused assets filenames above.`)
154+
155+
156+
} catch (err) {
157+
this.migrationError('Error fetching the assets references. Please double check the target space id.')
158+
}
159+
}
160+
161+
}

0 commit comments

Comments
 (0)