storyblok-backup 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/README.md +104 -6
- package/bin/storyblok-backup.mjs +53 -14
- package/bin/storyblok-restore.mjs +293 -0
- package/package.json +5 -3
package/.env.example
ADDED
package/README.md
CHANGED
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
A npx CLI tool to create a full backup of a space of the [Storyblok CMS](https://www.storyblok.com).
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
A restore tool to restore (create or update) resources is also included.
|
|
9
|
+
|
|
10
|
+
The backup script will fetch the following resources of a Storyblok space using the Management API and archive them in a zip file:
|
|
9
11
|
|
|
10
12
|
- Stories
|
|
13
|
+
- Collaborators
|
|
11
14
|
- Components
|
|
12
15
|
- Component groups
|
|
13
16
|
- Assets (optionally incl. original files)
|
|
14
17
|
- Asset folders
|
|
18
|
+
- Internal Tags
|
|
15
19
|
- Datasources (incl. entries)
|
|
16
20
|
- Space
|
|
17
21
|
- Space Roles
|
|
@@ -19,17 +23,31 @@ The script will fetch the following resources of a Storyblok space using the Man
|
|
|
19
23
|
- Activities
|
|
20
24
|
- Presets
|
|
21
25
|
- Field types
|
|
26
|
+
- Webhooks
|
|
27
|
+
- Workflows
|
|
22
28
|
- Workflow stages
|
|
23
29
|
- Workflow stage changes
|
|
24
|
-
- Custom workflows
|
|
25
30
|
- Releases
|
|
31
|
+
- Pipeline Branches
|
|
32
|
+
- Access Tokens
|
|
33
|
+
|
|
34
|
+
The restore script is able to individually restore the the resources from the backup files (via update or create) with the following exceptions:
|
|
35
|
+
|
|
36
|
+
- Assets: Creating assets is not supported
|
|
37
|
+
- Tasks: Currently not supported due to missing fields returned from management API
|
|
38
|
+
- Field types: Currently not supported
|
|
39
|
+
- Workflow stage changes: No update possible
|
|
40
|
+
- Access Tokens: Creating access tokens from backup makes no sense, since it will result in a new token-string.
|
|
26
41
|
|
|
27
42
|
## Installation
|
|
28
43
|
|
|
29
44
|
```shell
|
|
30
45
|
|
|
31
46
|
# simply auto-download and run via npx
|
|
47
|
+
## for backup:
|
|
32
48
|
$ npx storyblok-backup
|
|
49
|
+
## for restore:
|
|
50
|
+
$ npx -p storyblok-backup storyblok-restore
|
|
33
51
|
|
|
34
52
|
# or install globally
|
|
35
53
|
$ npm install -g storyblok-backup
|
|
@@ -46,21 +64,26 @@ $ pnpm add storyblok-backup
|
|
|
46
64
|
|
|
47
65
|
## Usage
|
|
48
66
|
|
|
67
|
+
### Backup
|
|
68
|
+
|
|
49
69
|
Call `npx storyblok-backup` with the following options:
|
|
50
70
|
|
|
51
|
-
|
|
71
|
+
#### Backup options
|
|
52
72
|
|
|
53
73
|
```text
|
|
54
74
|
--token <token> (required) Personal OAuth access token created
|
|
55
75
|
in the account settings of a Stoyblok user.
|
|
56
76
|
(NOT the Access Token of a Space!)
|
|
77
|
+
Alternatively, you can set the STORYBLOK_OAUTH_TOKEN environment variable.
|
|
57
78
|
--space <space_id> (required) ID of the space to backup
|
|
79
|
+
Alternatively, you can set the STORYBLOK_SPACE_ID environment variable.
|
|
58
80
|
--region <region> Region of the space. Possible values are:
|
|
59
81
|
- 'eu' (default): EU
|
|
60
82
|
- 'us': US
|
|
61
83
|
- 'ap': Australia
|
|
62
84
|
- 'ca': Canada
|
|
63
85
|
- 'cn': China
|
|
86
|
+
Alternatively, you can set the STORYBLOK_REGION environment variable.
|
|
64
87
|
--with-asset-files Downloads all files (assets) of the space. Defaults to false.
|
|
65
88
|
--output-dir <dir> Directory to write the backup to. Defaults to ./.output
|
|
66
89
|
(ATTENTION: Will fail if the directory already exists!)
|
|
@@ -72,7 +95,9 @@ Call `npx storyblok-backup` with the following options:
|
|
|
72
95
|
--help Show this help
|
|
73
96
|
```
|
|
74
97
|
|
|
75
|
-
|
|
98
|
+
OAuth token, space-id and region can be set via environment variables. You can also use a `.env` file in your project root for this (see `.env.example`).
|
|
99
|
+
|
|
100
|
+
#### Minimal backup example
|
|
76
101
|
|
|
77
102
|
```shell
|
|
78
103
|
npx storyblok-backup --token 1234567890abcdef --space 12345
|
|
@@ -80,7 +105,7 @@ npx storyblok-backup --token 1234567890abcdef --space 12345
|
|
|
80
105
|
|
|
81
106
|
This will create the folder `./.output/backup` and fetch all resources sorted into folders.
|
|
82
107
|
|
|
83
|
-
|
|
108
|
+
#### Maximal backup example
|
|
84
109
|
|
|
85
110
|
```shell
|
|
86
111
|
npx storyblok-backup \
|
|
@@ -97,7 +122,7 @@ npx storyblok-backup \
|
|
|
97
122
|
|
|
98
123
|
This will create the folder `./my-dir/backup`, fetch all resources (incl. the original file assets) sorted into folders, zip them to `./my-dir/daily-Y-m-d-H-i-s.zip`, and log every written file to console.
|
|
99
124
|
|
|
100
|
-
|
|
125
|
+
#### Continuous backup integration
|
|
101
126
|
|
|
102
127
|
You can e.g. use this script to create periodic backups of Storyblok spaces using GitHub Actions and artifacts.
|
|
103
128
|
|
|
@@ -158,6 +183,79 @@ If you create multiple workflows for daily, weekly and monthly backups, by chang
|
|
|
158
183
|
|
|
159
184
|
Also keep in mind, that there is a limit on artifact storage and runner minutes ([see GitHub docs](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#included-storage-and-minutes)).
|
|
160
185
|
|
|
186
|
+
### Restore
|
|
187
|
+
|
|
188
|
+
Call `npx -p storyblok-backup storyblok-restore` with the following options:
|
|
189
|
+
|
|
190
|
+
#### Restore options
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
--token <token> (required) Personal OAuth access token created
|
|
194
|
+
in the account settings of a Stoyblok user.
|
|
195
|
+
(NOT the Access Token of a Space!)
|
|
196
|
+
Alternatively, you can set the STORYBLOK_OAUTH_TOKEN environment variable.
|
|
197
|
+
--space <space_id> (required) ID of the space to backup
|
|
198
|
+
Alternatively, you can set the STORYBLOK_SPACE_ID environment variable.
|
|
199
|
+
--region <region> Region of the space. Possible values are:
|
|
200
|
+
- 'eu' (default): EU
|
|
201
|
+
- 'us': US
|
|
202
|
+
- 'ap': Australia
|
|
203
|
+
- 'ca': Canada
|
|
204
|
+
- 'cn': China
|
|
205
|
+
Alternatively, you can set the STORYBLOK_REGION environment variable.
|
|
206
|
+
--type <type> (required) Type of resource to restore. Possible values are:
|
|
207
|
+
- 'story'
|
|
208
|
+
- 'collaborator'
|
|
209
|
+
- 'component'
|
|
210
|
+
- 'component-group'
|
|
211
|
+
- 'asset'
|
|
212
|
+
- 'asset-folder'
|
|
213
|
+
- 'internal-tag'
|
|
214
|
+
- 'datasource'
|
|
215
|
+
- 'datasource-entries'
|
|
216
|
+
- 'space'
|
|
217
|
+
- 'space-role'
|
|
218
|
+
- 'preset'
|
|
219
|
+
- 'webhook'
|
|
220
|
+
- 'workflow'
|
|
221
|
+
- 'workflow-stage'
|
|
222
|
+
- 'release'
|
|
223
|
+
- 'pipeline-branch'
|
|
224
|
+
- 'access-token
|
|
225
|
+
--file <file> (required) File of resource to restore.
|
|
226
|
+
--publish Perform a publish after restore of a story (default=false).
|
|
227
|
+
--create Create a new resource instead of updating (default=false).
|
|
228
|
+
Not supported for assets.
|
|
229
|
+
--id <file> (required if type=datasource-entries and create is set)
|
|
230
|
+
ID of datasource the entries belong to.
|
|
231
|
+
--verbose Will show detailed result of the restore process.
|
|
232
|
+
--help Show this help
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### Minimal restore example
|
|
236
|
+
|
|
237
|
+
```shell
|
|
238
|
+
npx -p storyblok-backup storyblok-restore --token 1234567890abcdef --space 12345 --type story --file ./.output/backup/123456789.json
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
This will restore the story from the stated file by updating it.
|
|
242
|
+
|
|
243
|
+
#### Maximal restore example
|
|
244
|
+
|
|
245
|
+
```shell
|
|
246
|
+
npx -p storyblok-backup storyblok-restore \
|
|
247
|
+
--token 1234567890abcdef \
|
|
248
|
+
--space 12345 \
|
|
249
|
+
--region ap \
|
|
250
|
+
--type story \
|
|
251
|
+
--file ./.output/backup/123456789.json \
|
|
252
|
+
--publish \
|
|
253
|
+
--create \
|
|
254
|
+
--verbose
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
This will restore the story by creating a new story, immediately publish it, and log the API result to console.
|
|
258
|
+
|
|
161
259
|
## License
|
|
162
260
|
|
|
163
261
|
This package is open-sourced software licensed under the [MIT license](https://github.com/webflorist/storyblok-backup/blob/main/LICENSE.).
|
package/bin/storyblok-backup.mjs
CHANGED
|
@@ -7,9 +7,12 @@ import zipLib from 'zip-lib'
|
|
|
7
7
|
import minimist from 'minimist'
|
|
8
8
|
import StoryblokClient from 'storyblok-js-client'
|
|
9
9
|
import { performance } from 'perf_hooks'
|
|
10
|
+
import dotenvx from '@dotenvx/dotenvx'
|
|
10
11
|
|
|
11
12
|
const startTime = performance.now()
|
|
12
13
|
|
|
14
|
+
dotenvx.config({ quiet: true })
|
|
15
|
+
|
|
13
16
|
const args = minimist(process.argv.slice(2))
|
|
14
17
|
|
|
15
18
|
if ('help' in args) {
|
|
@@ -20,13 +23,16 @@ OPTIONS
|
|
|
20
23
|
--token <token> (required) Personal OAuth access token created
|
|
21
24
|
in the account settings of a Stoyblok user.
|
|
22
25
|
(NOT the Access Token of a Space!)
|
|
26
|
+
Alternatively, you can set the STORYBLOK_OAUTH_TOKEN environment variable.
|
|
23
27
|
--space <space_id> (required) ID of the space to backup
|
|
28
|
+
Alternatively, you can set the STORYBLOK_SPACE_ID environment variable.
|
|
24
29
|
--region <region> Region of the space. Possible values are:
|
|
25
30
|
- 'eu' (default): EU
|
|
26
31
|
- 'us': US
|
|
27
32
|
- 'ap': Australia
|
|
28
33
|
- 'ca': Canada
|
|
29
34
|
- 'cn': China
|
|
35
|
+
Alternatively, you can set the STORYBLOK_REGION environment variable.
|
|
30
36
|
--with-asset-files Downloads all files (assets) of the space. Defaults to false.
|
|
31
37
|
--output-dir <dir> Directory to write the backup to. Defaults to ./.output
|
|
32
38
|
(ATTENTION: Will fail if the directory already exists!)
|
|
@@ -55,21 +61,30 @@ MAXIMAL EXAMPLE
|
|
|
55
61
|
process.exit(0)
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
if (!('token' in args)) {
|
|
64
|
+
if (!('token' in args) && !process.env.STORYBLOK_OAUTH_TOKEN) {
|
|
59
65
|
console.log(
|
|
60
|
-
'Error: State your oauth token via the --token argument. Use --help to find out more.'
|
|
66
|
+
'Error: State your oauth token via the --token argument or the environment variable STORYBLOK_OAUTH_TOKEN. Use --help to find out more.'
|
|
61
67
|
)
|
|
62
68
|
process.exit(1)
|
|
63
69
|
}
|
|
70
|
+
const oauthToken = args.token || process.env.STORYBLOK_OAUTH_TOKEN
|
|
64
71
|
|
|
65
|
-
if (!('space' in args)) {
|
|
66
|
-
console.log(
|
|
72
|
+
if (!('space' in args) && !process.env.STORYBLOK_SPACE_ID) {
|
|
73
|
+
console.log(
|
|
74
|
+
'Error: State your space id via the --space argument or the environment variable STORYBLOK_SPACE_ID. Use --help to find out more.'
|
|
75
|
+
)
|
|
67
76
|
process.exit(1)
|
|
68
77
|
}
|
|
78
|
+
const spaceId = args.space || process.env.STORYBLOK_SPACE_ID
|
|
69
79
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
process.
|
|
80
|
+
let region = 'eu'
|
|
81
|
+
if ('region' in args || process.env.STORYBLOK_REGION) {
|
|
82
|
+
region = args.region || process.env.STORYBLOK_REGION
|
|
83
|
+
|
|
84
|
+
if (!['eu', 'us', 'ap', 'ca', 'cn'].includes(region)) {
|
|
85
|
+
console.log('Error: Invalid region parameter stated. Use --help to find out more.')
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
const verbose = 'verbose' in args
|
|
@@ -85,10 +100,6 @@ if (fs.existsSync(outputDir) && !('force' in args)) {
|
|
|
85
100
|
process.exit(1)
|
|
86
101
|
}
|
|
87
102
|
|
|
88
|
-
const spaceId = args.space
|
|
89
|
-
|
|
90
|
-
const region = args['region'] || 'eu'
|
|
91
|
-
|
|
92
103
|
const filePrefix = args['zip-prefix'] || 'backup'
|
|
93
104
|
|
|
94
105
|
const fileName =
|
|
@@ -112,7 +123,7 @@ if ('create-zip' in args) {
|
|
|
112
123
|
|
|
113
124
|
// Init Management API
|
|
114
125
|
const StoryblokMAPI = new StoryblokClient({
|
|
115
|
-
oauthToken:
|
|
126
|
+
oauthToken: oauthToken,
|
|
116
127
|
region: region,
|
|
117
128
|
})
|
|
118
129
|
|
|
@@ -126,20 +137,25 @@ fs.mkdirSync(backupDir, { recursive: true })
|
|
|
126
137
|
|
|
127
138
|
const resources = [
|
|
128
139
|
'stories',
|
|
140
|
+
'collaborators',
|
|
129
141
|
'components',
|
|
130
142
|
'component-groups',
|
|
131
143
|
'assets',
|
|
132
144
|
'asset-folders',
|
|
145
|
+
'internal-tags',
|
|
133
146
|
'datasources',
|
|
134
147
|
'space-roles',
|
|
135
148
|
'tasks',
|
|
136
149
|
'activities',
|
|
137
150
|
'presets',
|
|
138
151
|
'field-types',
|
|
152
|
+
'webhooks',
|
|
139
153
|
'workflow-stages',
|
|
140
154
|
'workflow-stage-changes',
|
|
141
155
|
'workflows',
|
|
142
156
|
'releases',
|
|
157
|
+
'pipeline-branches',
|
|
158
|
+
'access-tokens',
|
|
143
159
|
]
|
|
144
160
|
resources.forEach((resource) => fs.mkdirSync(`${backupDir}/${resource}`))
|
|
145
161
|
|
|
@@ -170,7 +186,7 @@ const writeJson = (folder, file, content) => {
|
|
|
170
186
|
outputFile += `/${folder}`
|
|
171
187
|
}
|
|
172
188
|
outputFile += `/${file}.json`
|
|
173
|
-
fs.
|
|
189
|
+
fs.writeFileSync(outputFile, JSON.stringify(content, null, 2), (error) => {
|
|
174
190
|
if (error) {
|
|
175
191
|
throw error
|
|
176
192
|
}
|
|
@@ -203,7 +219,10 @@ await StoryblokMAPI.getAll(`spaces/${spaceId}/stories`)
|
|
|
203
219
|
.then(async (stories) => {
|
|
204
220
|
for (const story of stories) {
|
|
205
221
|
await StoryblokMAPI.get(`spaces/${spaceId}/stories/${story.id}`)
|
|
206
|
-
.then((response) =>
|
|
222
|
+
.then((response) => {
|
|
223
|
+
delete response.data.story.preview_token
|
|
224
|
+
writeJson('stories', story.id, response.data.story)
|
|
225
|
+
})
|
|
207
226
|
.catch((error) => {
|
|
208
227
|
throw error
|
|
209
228
|
})
|
|
@@ -213,6 +232,10 @@ await StoryblokMAPI.getAll(`spaces/${spaceId}/stories`)
|
|
|
213
232
|
throw error
|
|
214
233
|
})
|
|
215
234
|
|
|
235
|
+
// Fetch all collaborators
|
|
236
|
+
console.log(`Fetching collaborators`)
|
|
237
|
+
await defaultFetch('collaborators', 'collaborators', 'user_id')
|
|
238
|
+
|
|
216
239
|
// Fetch all components
|
|
217
240
|
console.log(`Fetching components`)
|
|
218
241
|
await defaultFetch('components', 'components', 'name')
|
|
@@ -242,6 +265,10 @@ await StoryblokMAPI.getAll(`spaces/${spaceId}/assets`)
|
|
|
242
265
|
console.log(`Fetching asset-folders`)
|
|
243
266
|
await defaultFetch('asset_folders', 'asset-folders', 'id')
|
|
244
267
|
|
|
268
|
+
// Fetch all internal-tags
|
|
269
|
+
console.log(`Fetching internal-tags`)
|
|
270
|
+
await defaultFetch('internal_tags', 'internal-tags', 'id')
|
|
271
|
+
|
|
245
272
|
// Fetch all datasources (including entries)
|
|
246
273
|
console.log(`Fetching datasources`)
|
|
247
274
|
await StoryblokMAPI.getAll(`spaces/${spaceId}/datasources`)
|
|
@@ -283,6 +310,10 @@ await defaultFetch('presets', 'presets', 'id')
|
|
|
283
310
|
console.log(`Fetching field-types`)
|
|
284
311
|
await defaultFetch('field_types', 'field-types', 'name')
|
|
285
312
|
|
|
313
|
+
// Fetch all webhooks
|
|
314
|
+
console.log(`Fetching webhooks`)
|
|
315
|
+
await defaultFetch('webhook_endpoints', 'webhooks', 'id')
|
|
316
|
+
|
|
286
317
|
// Fetch all workflow-stages
|
|
287
318
|
console.log(`Fetching workflow-stages`)
|
|
288
319
|
await defaultFetch('workflow_stages', 'workflow-stages', 'id')
|
|
@@ -299,6 +330,14 @@ await defaultFetch('workflows', 'workflows', 'id')
|
|
|
299
330
|
console.log(`Fetching releases`)
|
|
300
331
|
await defaultFetch('releases', 'releases', 'id')
|
|
301
332
|
|
|
333
|
+
// Fetch all pipeline branches
|
|
334
|
+
console.log(`Fetching pipeline branches`)
|
|
335
|
+
await defaultFetch('branches', 'pipeline-branches', 'id')
|
|
336
|
+
|
|
337
|
+
// Fetch all access tokens
|
|
338
|
+
console.log(`Fetching access tokens`)
|
|
339
|
+
await defaultFetch('api_keys', 'access-tokens', 'id')
|
|
340
|
+
|
|
302
341
|
// Create zip file
|
|
303
342
|
if ('create-zip' in args) {
|
|
304
343
|
console.log(`Creating zip file`)
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import minimist from 'minimist'
|
|
5
|
+
import StoryblokClient from 'storyblok-js-client'
|
|
6
|
+
import { performance } from 'perf_hooks'
|
|
7
|
+
import dotenvx from '@dotenvx/dotenvx'
|
|
8
|
+
|
|
9
|
+
const startTime = performance.now()
|
|
10
|
+
|
|
11
|
+
dotenvx.config({ quiet: true })
|
|
12
|
+
|
|
13
|
+
const resourceTypes = [
|
|
14
|
+
'story',
|
|
15
|
+
'collaborator',
|
|
16
|
+
'component',
|
|
17
|
+
'component-group',
|
|
18
|
+
'asset',
|
|
19
|
+
'asset-folder',
|
|
20
|
+
'internal-tag',
|
|
21
|
+
'datasource',
|
|
22
|
+
'datasource-entries',
|
|
23
|
+
'space',
|
|
24
|
+
'space-role',
|
|
25
|
+
//'task', // Currently not supported due to missing fields returned from management API
|
|
26
|
+
'preset',
|
|
27
|
+
// 'field-type',
|
|
28
|
+
'webhook',
|
|
29
|
+
'workflow',
|
|
30
|
+
'workflow-stage',
|
|
31
|
+
'release',
|
|
32
|
+
'pipeline-branch',
|
|
33
|
+
'access-token',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const args = minimist(process.argv.slice(2))
|
|
37
|
+
|
|
38
|
+
if ('help' in args) {
|
|
39
|
+
console.log(`USAGE
|
|
40
|
+
$ npx -p storyblok-backup storyblok-restore
|
|
41
|
+
|
|
42
|
+
OPTIONS
|
|
43
|
+
--token <token> (required) Personal OAuth access token created
|
|
44
|
+
in the account settings of a Stoyblok user.
|
|
45
|
+
(NOT the Access Token of a Space!)
|
|
46
|
+
Alternatively, you can set the STORYBLOK_OAUTH_TOKEN environment variable.
|
|
47
|
+
--space <space_id> (required) ID of the space to backup
|
|
48
|
+
Alternatively, you can set the STORYBLOK_SPACE_ID environment variable.
|
|
49
|
+
--region <region> Region of the space. Possible values are:
|
|
50
|
+
- 'eu' (default): EU
|
|
51
|
+
- 'us': US
|
|
52
|
+
- 'ap': Australia
|
|
53
|
+
- 'ca': Canada
|
|
54
|
+
- 'cn': China
|
|
55
|
+
Alternatively, you can set the STORYBLOK_REGION environment variable.
|
|
56
|
+
--type <type> (required) Type of resource to restore. Possible values are:
|
|
57
|
+
- '${resourceTypes.join("'\n - '")}
|
|
58
|
+
--file <file> (required) File of resource to restore.
|
|
59
|
+
--publish Perform a publish after restore of a story (default=false).
|
|
60
|
+
--create Create a new resource instead of updating (default=false).
|
|
61
|
+
Not supported for assets.
|
|
62
|
+
--id <file> (required if type=datasource-entries and create is set)
|
|
63
|
+
ID of datasource the entries belong to.
|
|
64
|
+
--verbose Will show detailed result of the restore process.
|
|
65
|
+
--help Show this help
|
|
66
|
+
|
|
67
|
+
MINIMAL EXAMPLE
|
|
68
|
+
$ npx -p storyblok-backup storyblok-restore --token 1234567890abcdef --space 12345 --type story --file ./.output/backup/123456789.json
|
|
69
|
+
|
|
70
|
+
MAXIMAL EXAMPLE
|
|
71
|
+
$ npx -p storyblok-backup storyblok-restore \\
|
|
72
|
+
--token 1234567890abcdef \\
|
|
73
|
+
--space 12345 \\
|
|
74
|
+
--region ap \\
|
|
75
|
+
--type story \\
|
|
76
|
+
--file ./.output/backup/123456789.json \\
|
|
77
|
+
--publish \\
|
|
78
|
+
--create \\
|
|
79
|
+
--verbose
|
|
80
|
+
`)
|
|
81
|
+
process.exit(0)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!('token' in args) && !process.env.STORYBLOK_OAUTH_TOKEN) {
|
|
85
|
+
console.log(
|
|
86
|
+
'Error: State your oauth token via the --token argument or the environment variable STORYBLOK_OAUTH_TOKEN. Use --help to find out more.'
|
|
87
|
+
)
|
|
88
|
+
process.exit(1)
|
|
89
|
+
}
|
|
90
|
+
const oauthToken = args.token || process.env.STORYBLOK_OAUTH_TOKEN
|
|
91
|
+
|
|
92
|
+
if (!('space' in args) && !process.env.STORYBLOK_SPACE_ID) {
|
|
93
|
+
console.log(
|
|
94
|
+
'Error: State your space id via the --space argument or the environment variable STORYBLOK_SPACE_ID. Use --help to find out more.'
|
|
95
|
+
)
|
|
96
|
+
process.exit(1)
|
|
97
|
+
}
|
|
98
|
+
const spaceId = args.space || process.env.STORYBLOK_SPACE_ID
|
|
99
|
+
|
|
100
|
+
let region = 'eu'
|
|
101
|
+
if ('region' in args || process.env.STORYBLOK_REGION) {
|
|
102
|
+
region = args.region || process.env.STORYBLOK_REGION
|
|
103
|
+
|
|
104
|
+
if (!['eu', 'us', 'ap', 'ca', 'cn'].includes(region)) {
|
|
105
|
+
console.log('Error: Invalid region parameter stated. Use --help to find out more.')
|
|
106
|
+
process.exit(1)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!('type' in args)) {
|
|
111
|
+
console.log(
|
|
112
|
+
'Error: State the resource type to restore via the --type argument. Use --help to find out more.'
|
|
113
|
+
)
|
|
114
|
+
process.exit(1)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!resourceTypes.includes(args.type)) {
|
|
118
|
+
console.log(`Error: Invalid resource type "${args.type}". Use --help to find out more.`)
|
|
119
|
+
process.exit(1)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!('file' in args)) {
|
|
123
|
+
console.log(
|
|
124
|
+
'Error: State the resource file to restore via the --file argument. Use --help to find out more.'
|
|
125
|
+
)
|
|
126
|
+
process.exit(1)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!fs.existsSync(args.file)) {
|
|
130
|
+
console.log(`Error: Stated file "${args.file}" does not exist.`)
|
|
131
|
+
process.exit(1)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const verbose = 'verbose' in args
|
|
135
|
+
|
|
136
|
+
const publish = 'publish' in args
|
|
137
|
+
|
|
138
|
+
const create = 'create' in args
|
|
139
|
+
|
|
140
|
+
// Init Management API
|
|
141
|
+
const StoryblokMAPI = new StoryblokClient({
|
|
142
|
+
oauthToken: oauthToken,
|
|
143
|
+
region: region,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Function to perform a default single resource restore
|
|
147
|
+
const defaultSingleRestore = async (type, id, params) => {
|
|
148
|
+
if (publish) {
|
|
149
|
+
params.publish = 1
|
|
150
|
+
}
|
|
151
|
+
let url = `spaces`
|
|
152
|
+
|
|
153
|
+
if (!(create && args.type === 'space')) {
|
|
154
|
+
url = `${url}/${spaceId}`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (type) {
|
|
158
|
+
url = `${url}/${type}`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (create) {
|
|
162
|
+
await StoryblokMAPI.post(url, params)
|
|
163
|
+
.then((response) => {
|
|
164
|
+
console.log(`Created "${type}" resource.`)
|
|
165
|
+
if (verbose) {
|
|
166
|
+
console.log('Result:')
|
|
167
|
+
console.log(response.data)
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
.catch((error) => {
|
|
171
|
+
throw error
|
|
172
|
+
})
|
|
173
|
+
} else {
|
|
174
|
+
if (id) {
|
|
175
|
+
url = `${url}/${id}`
|
|
176
|
+
}
|
|
177
|
+
await StoryblokMAPI.put(url, params)
|
|
178
|
+
.then((response) => {
|
|
179
|
+
console.log(`Updated "${type}" resource with id "${id}".`)
|
|
180
|
+
if (verbose) {
|
|
181
|
+
console.log('Result:')
|
|
182
|
+
console.log(response.data)
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
.catch((error) => {
|
|
186
|
+
throw error
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const resource = JSON.parse(fs.readFileSync(args.file, 'utf8'))
|
|
192
|
+
|
|
193
|
+
switch (args.type) {
|
|
194
|
+
case 'story':
|
|
195
|
+
delete resource.updated_at
|
|
196
|
+
await defaultSingleRestore('stories', resource.id, { story: resource })
|
|
197
|
+
break
|
|
198
|
+
case 'collaborator':
|
|
199
|
+
await defaultSingleRestore(
|
|
200
|
+
'collaborators',
|
|
201
|
+
resource.id,
|
|
202
|
+
create
|
|
203
|
+
? {
|
|
204
|
+
email: resource.user.userid,
|
|
205
|
+
role: resource.role,
|
|
206
|
+
space_id: resource.space_id,
|
|
207
|
+
permissions: resource.permissions,
|
|
208
|
+
space_role_ids: resource.space_role_ids,
|
|
209
|
+
allow_multiple_roles_creation: resource.role === 'multi',
|
|
210
|
+
}
|
|
211
|
+
: { collaborator: resource }
|
|
212
|
+
)
|
|
213
|
+
break
|
|
214
|
+
case 'component':
|
|
215
|
+
await defaultSingleRestore('components', resource.id, { component: resource })
|
|
216
|
+
break
|
|
217
|
+
case 'component-group':
|
|
218
|
+
await defaultSingleRestore('component_groups', resource.id, { component_group: resource })
|
|
219
|
+
break
|
|
220
|
+
case 'asset':
|
|
221
|
+
if (create) {
|
|
222
|
+
console.log('Error: Creating assets is not supported.')
|
|
223
|
+
process.exit(1)
|
|
224
|
+
}
|
|
225
|
+
await defaultSingleRestore('assets', resource.id, { asset: resource })
|
|
226
|
+
break
|
|
227
|
+
case 'asset-folder':
|
|
228
|
+
await defaultSingleRestore('asset_folders', resource.id, { asset_folder: resource })
|
|
229
|
+
break
|
|
230
|
+
case 'internal-tag':
|
|
231
|
+
await defaultSingleRestore('internal_tags', resource.id, { internal_tag: resource })
|
|
232
|
+
break
|
|
233
|
+
case 'datasource':
|
|
234
|
+
await defaultSingleRestore('datasources', resource.id, { datasource: resource })
|
|
235
|
+
break
|
|
236
|
+
case 'datasource-entries':
|
|
237
|
+
if (create && !('id' in args)) {
|
|
238
|
+
console.log(
|
|
239
|
+
'Error: State the datasource ID via the --id argument. Use --help to find out more.'
|
|
240
|
+
)
|
|
241
|
+
process.exit(1)
|
|
242
|
+
}
|
|
243
|
+
for (const entry of resource) {
|
|
244
|
+
if (create) {
|
|
245
|
+
entry.datasource_id = args.id
|
|
246
|
+
}
|
|
247
|
+
await defaultSingleRestore('datasource_entries', entry.id, {
|
|
248
|
+
datasource_entry: entry,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
break
|
|
252
|
+
case 'space':
|
|
253
|
+
await defaultSingleRestore(null, null, { space: resource })
|
|
254
|
+
break
|
|
255
|
+
case 'space-role':
|
|
256
|
+
await defaultSingleRestore('space_roles', resource.id, { space_role: resource })
|
|
257
|
+
break
|
|
258
|
+
case 'task':
|
|
259
|
+
await defaultSingleRestore('tasks', resource.id, { task: resource })
|
|
260
|
+
break
|
|
261
|
+
case 'preset':
|
|
262
|
+
await defaultSingleRestore('presets', resource.id, { preset: resource })
|
|
263
|
+
break
|
|
264
|
+
case 'webhook':
|
|
265
|
+
await defaultSingleRestore('webhook_endpoints', resource.id, { webhook_endpoint: resource })
|
|
266
|
+
break
|
|
267
|
+
case 'workflow':
|
|
268
|
+
await defaultSingleRestore('workflows', resource.id, { workflow: resource })
|
|
269
|
+
break
|
|
270
|
+
case 'workflow-stage':
|
|
271
|
+
await defaultSingleRestore('workflow_stages', resource.id, { workflow_stage: resource })
|
|
272
|
+
break
|
|
273
|
+
case 'release':
|
|
274
|
+
await defaultSingleRestore('releases', resource.id, { release: resource })
|
|
275
|
+
break
|
|
276
|
+
case 'pipeline-branch':
|
|
277
|
+
await defaultSingleRestore('branches', resource.id, { branch: resource })
|
|
278
|
+
break
|
|
279
|
+
case 'access-token':
|
|
280
|
+
if (create) {
|
|
281
|
+
console.log(
|
|
282
|
+
'Error: Creating access-tokens from backup is not possible, since it will result in a new token.'
|
|
283
|
+
)
|
|
284
|
+
process.exit(1)
|
|
285
|
+
}
|
|
286
|
+
await defaultSingleRestore('api_keys', resource.id, { api_key: resource })
|
|
287
|
+
break
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const endTime = performance.now()
|
|
291
|
+
|
|
292
|
+
console.log(`Restore successful in ${Math.round((endTime - startTime) / 1000)} seconds.`)
|
|
293
|
+
process.exit(0)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "storyblok-backup",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "npx CLI tool to create a full backup of a Storyblok space",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "npx CLI tool to create a full backup of a Storyblok space and restore single resources from it.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"upgrade": "npx npm-check-updates -i -u && pnpm install",
|
|
7
7
|
"lint:js": "eslint --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .lintignore .",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"lintfix": "pnpm lintfix:js && pnpm lintfix:prettier"
|
|
13
13
|
},
|
|
14
14
|
"bin": {
|
|
15
|
-
"storyblok-backup": "bin/storyblok-backup.mjs"
|
|
15
|
+
"storyblok-backup": "bin/storyblok-backup.mjs",
|
|
16
|
+
"storyblok-restore": "bin/storyblok-restore.mjs"
|
|
16
17
|
},
|
|
17
18
|
"repository": {
|
|
18
19
|
"type": "git",
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
"prettier": "^3.3.2"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
44
|
+
"@dotenvx/dotenvx": "^1.6.2",
|
|
43
45
|
"minimist": "^1.2.8",
|
|
44
46
|
"storyblok-js-client": "^6.7.3",
|
|
45
47
|
"zip-lib": "^1.0.4"
|