storyblok-backup
Version:
npx CLI tool to create a full backup of a Storyblok space and restore single resources from it.
354 lines (323 loc) • 10.4 kB
JavaScript
/* eslint-disable no-console */
import fs from 'fs'
import minimist from 'minimist'
import StoryblokClient from 'storyblok-js-client'
import { performance } from 'perf_hooks'
import dotenvx from '@dotenvx/dotenvx'
const startTime = performance.now()
dotenvx.config({ quiet: true })
const resourceTypes = [
'story',
'collaborator',
'component',
'component-group',
'asset',
'asset-folder',
'internal-tag',
'datasource',
'datasource-entries',
'space',
'space-role',
'task',
'preset',
// 'field-type',
'webhook',
'workflow',
'workflow-stage',
'release',
'pipeline-branch',
'access-token',
]
const args = minimist(process.argv.slice(2))
if ('help' in args) {
console.log(`USAGE
$ npx storyblok-restore
OPTIONS
--token <token> (required) Personal OAuth access token created
in the account settings of a Stoyblok user.
(NOT the Access Token of a Space!)
Alternatively, you can set the STORYBLOK_OAUTH_TOKEN environment variable.
--space <space_id> (required) ID of the space to backup
Alternatively, you can set the STORYBLOK_SPACE_ID environment variable.
--region <region> Region of the space. Possible values are:
- 'eu' (default): EU
- 'us': US
- 'ap': Australia
- 'ca': Canada
- 'cn': China
Alternatively, you can set the STORYBLOK_REGION environment variable.
--type <type> (required) Type of resource to restore. Possible values are:
- '${resourceTypes.join("'\n - '")}'
--file <file> (required) File of resource to restore.
--publish Perform a publish after restore of a story (default=false).
--create Create a new resource instead of updating (default=false).
Not supported for assets.
--propagate Propagate new story UUID to referencing stories (default=false).
Usable with create and stories. A create results in a new ID and UUID.
This option will update all stories referencing the old
UUID (as stated in the backup-json) with the new one.
--id <file> (required if type=datasource-entries and create is set)
ID of datasource the entries belong to.
--verbose Will show detailed result of the restore process.
--help Show this help
MINIMAL EXAMPLE
$ npx storyblok-restore --token 1234567890abcdef --space 12345 --type story --file ./.output/backup/123456789.json
MAXIMAL EXAMPLE
$ npx storyblok-restore \\
--token 1234567890abcdef \\
--space 12345 \\
--region ap \\
--type story \\
--file ./.output/backup/123456789.json \\
--publish \\
--create \\
--propagate \\
--verbose
`)
process.exit(0)
}
if (!('token' in args) && !process.env.STORYBLOK_OAUTH_TOKEN) {
console.log(
'Error: State your oauth token via the --token argument or the environment variable STORYBLOK_OAUTH_TOKEN. Use --help to find out more.'
)
process.exit(1)
}
const oauthToken = args.token || process.env.STORYBLOK_OAUTH_TOKEN
if (!('space' in args) && !process.env.STORYBLOK_SPACE_ID) {
console.log(
'Error: State your space id via the --space argument or the environment variable STORYBLOK_SPACE_ID. Use --help to find out more.'
)
process.exit(1)
}
const spaceId = args.space || process.env.STORYBLOK_SPACE_ID
let region = 'eu'
if ('region' in args || process.env.STORYBLOK_REGION) {
region = args.region || process.env.STORYBLOK_REGION
if (!['eu', 'us', 'ap', 'ca', 'cn'].includes(region)) {
console.log('Error: Invalid region parameter stated. Use --help to find out more.')
process.exit(1)
}
}
if (!('type' in args)) {
console.log(
'Error: State the resource type to restore via the --type argument. Use --help to find out more.'
)
process.exit(1)
}
if (!resourceTypes.includes(args.type)) {
console.log(`Error: Invalid resource type "${args.type}". Use --help to find out more.`)
process.exit(1)
}
if (!('file' in args)) {
console.log(
'Error: State the resource file to restore via the --file argument. Use --help to find out more.'
)
process.exit(1)
}
if (!fs.existsSync(args.file)) {
console.log(`Error: Stated file "${args.file}" does not exist.`)
process.exit(1)
}
const verbose = 'verbose' in args
const publish = 'publish' in args
const create = 'create' in args
const propagate = 'propagate' in args
if (propagate && !create) {
console.log('Error: Propagate is only usable with create. Use --help to find out more.')
process.exit(1)
}
if (propagate && args.type !== 'story') {
console.log(
'Error: Propagate is only usable with story resources. Use --help to find out more.'
)
process.exit(1)
}
// Output General information
console.log('')
console.log(`Restoring backup in space ${spaceId}:`)
console.log(`- resource type: ${args.type}`)
console.log(`- file: ${args.file}`)
if ('id' in args) {
console.log(`- datasource id: ${args.id}`)
}
console.log(`- mode: ${'create' in args ? 'create' : 'update'}`)
console.log(`- publish: ${'publish' in args ? 'yes' : 'no'}`)
console.log(`- propagate: ${'propagate' in args ? 'yes' : 'no'}`)
console.log('')
// Init Management API
const StoryblokMAPI = new StoryblokClient({
oauthToken: oauthToken,
region: region,
})
// Function to perform a default single resource restore
const defaultSingleRestore = async (type, id, params, forceUpdate) => {
if (publish) {
params.publish = 1
}
let url = `spaces`
if (!(create && args.type === 'space')) {
url = `${url}/${spaceId}`
}
if (type) {
url = `${url}/${type}`
}
if (create && !forceUpdate) {
return await StoryblokMAPI.post(url, params)
.then((response) => {
console.log(`Created "${type}" resource.`)
if (verbose) {
console.log('Result:')
console.log(response.data)
}
return response.data
})
.catch((error) => {
throw error
})
} else {
if (id) {
url = `${url}/${id}`
}
return await StoryblokMAPI.put(url, params)
.then((response) => {
console.log(`Updated "${type}" resource with id "${id}".`)
if (verbose) {
console.log('Result:')
console.log(response.data)
}
return response.data
})
.catch((error) => {
throw error
})
}
}
const resource = JSON.parse(fs.readFileSync(args.file, 'utf8'))
switch (args.type) {
case 'story':
delete resource.updated_at
const result = await defaultSingleRestore('stories', resource.id, { story: resource })
if (create && propagate) {
const oldUuid = resource.uuid
const newUuid = result.story.uuid
const newId = result.story.id
console.log(`Propagating UUID change from "${oldUuid}" to "${newUuid}":`)
const referencingStories = await StoryblokMAPI.getAll(`spaces/${spaceId}/stories`, {
reference_search: oldUuid,
excluding_ids: [newId],
})
for (const referencingStory of referencingStories) {
const fullReferencingStoryResult = await StoryblokMAPI.get(
`spaces/${spaceId}/stories/${referencingStory.id}`
)
await defaultSingleRestore(
'stories',
referencingStory.id,
{
story: JSON.parse(
JSON.stringify(fullReferencingStoryResult.data.story).replaceAll(
oldUuid,
newUuid
)
),
},
true
)
}
}
break
case 'collaborator':
await defaultSingleRestore(
'collaborators',
resource.id,
create
? {
email: resource.user.userid,
role: resource.role,
space_id: resource.space_id,
permissions: resource.permissions,
space_role_ids: resource.space_role_ids,
allow_multiple_roles_creation: resource.role === 'multi',
}
: { collaborator: resource }
)
break
case 'component':
await defaultSingleRestore('components', resource.id, { component: resource })
break
case 'component-group':
await defaultSingleRestore('component_groups', resource.id, { component_group: resource })
break
case 'asset':
if (create) {
console.log('Error: Creating assets is not supported.')
process.exit(1)
}
await defaultSingleRestore('assets', resource.id, { asset: resource })
break
case 'asset-folder':
await defaultSingleRestore('asset_folders', resource.id, { asset_folder: resource })
break
case 'internal-tag':
await defaultSingleRestore('internal_tags', resource.id, { internal_tag: resource })
break
case 'datasource':
await defaultSingleRestore('datasources', resource.id, { datasource: resource })
break
case 'datasource-entries':
if (create && !('id' in args)) {
console.log(
'Error: State the datasource ID via the --id argument. Use --help to find out more.'
)
process.exit(1)
}
for (const entry of resource) {
if (create) {
entry.datasource_id = args.id
}
await defaultSingleRestore('datasource_entries', entry.id, {
datasource_entry: entry,
})
}
break
case 'space':
await defaultSingleRestore(null, null, { space: resource })
break
case 'space-role':
await defaultSingleRestore('space_roles', resource.id, { space_role: resource })
break
case 'task':
await defaultSingleRestore('tasks', resource.id, { task: resource })
break
case 'preset':
await defaultSingleRestore('presets', resource.id, { preset: resource })
break
case 'webhook':
await defaultSingleRestore('webhook_endpoints', resource.id, { webhook_endpoint: resource })
break
case 'workflow':
await defaultSingleRestore('workflows', resource.id, { workflow: resource })
break
case 'workflow-stage':
await defaultSingleRestore('workflow_stages', resource.id, { workflow_stage: resource })
break
case 'release':
await defaultSingleRestore('releases', resource.id, { release: resource })
break
case 'pipeline-branch':
await defaultSingleRestore('branches', resource.id, { branch: resource })
break
case 'access-token':
if (create) {
console.log(
'Error: Creating access-tokens from backup is not possible, since it will result in a new token.'
)
process.exit(1)
}
await defaultSingleRestore('api_keys', resource.id, { api_key: resource })
break
}
const endTime = performance.now()
console.log(`Restore successful in ${Math.round((endTime - startTime) / 1000)} seconds.`)
process.exit(0)