storyblok-backup 0.0.3 → 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 +111 -6
- package/bin/storyblok-backup.mjs +64 -10
- package/bin/storyblok-restore.mjs +293 -0
- package/package.json +10 -9
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,15 +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.
|
|
80
|
+
--region <region> Region of the space. Possible values are:
|
|
81
|
+
- 'eu' (default): EU
|
|
82
|
+
- 'us': US
|
|
83
|
+
- 'ap': Australia
|
|
84
|
+
- 'ca': Canada
|
|
85
|
+
- 'cn': China
|
|
86
|
+
Alternatively, you can set the STORYBLOK_REGION environment variable.
|
|
58
87
|
--with-asset-files Downloads all files (assets) of the space. Defaults to false.
|
|
59
88
|
--output-dir <dir> Directory to write the backup to. Defaults to ./.output
|
|
60
89
|
(ATTENTION: Will fail if the directory already exists!)
|
|
@@ -66,7 +95,9 @@ Call `npx storyblok-backup` with the following options:
|
|
|
66
95
|
--help Show this help
|
|
67
96
|
```
|
|
68
97
|
|
|
69
|
-
|
|
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
|
|
70
101
|
|
|
71
102
|
```shell
|
|
72
103
|
npx storyblok-backup --token 1234567890abcdef --space 12345
|
|
@@ -74,12 +105,13 @@ npx storyblok-backup --token 1234567890abcdef --space 12345
|
|
|
74
105
|
|
|
75
106
|
This will create the folder `./.output/backup` and fetch all resources sorted into folders.
|
|
76
107
|
|
|
77
|
-
|
|
108
|
+
#### Maximal backup example
|
|
78
109
|
|
|
79
110
|
```shell
|
|
80
111
|
npx storyblok-backup \
|
|
81
112
|
--token 1234567890abcdef \
|
|
82
113
|
--space 12345 \
|
|
114
|
+
--region ap \\
|
|
83
115
|
--with-asset-files \
|
|
84
116
|
--output-dir ./my-dir \
|
|
85
117
|
--force \
|
|
@@ -90,7 +122,7 @@ npx storyblok-backup \
|
|
|
90
122
|
|
|
91
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.
|
|
92
124
|
|
|
93
|
-
|
|
125
|
+
#### Continuous backup integration
|
|
94
126
|
|
|
95
127
|
You can e.g. use this script to create periodic backups of Storyblok spaces using GitHub Actions and artifacts.
|
|
96
128
|
|
|
@@ -151,6 +183,79 @@ If you create multiple workflows for daily, weekly and monthly backups, by chang
|
|
|
151
183
|
|
|
152
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)).
|
|
153
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
|
+
|
|
154
259
|
## License
|
|
155
260
|
|
|
156
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,7 +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.
|
|
29
|
+
--region <region> Region of the space. Possible values are:
|
|
30
|
+
- 'eu' (default): EU
|
|
31
|
+
- 'us': US
|
|
32
|
+
- 'ap': Australia
|
|
33
|
+
- 'ca': Canada
|
|
34
|
+
- 'cn': China
|
|
35
|
+
Alternatively, you can set the STORYBLOK_REGION environment variable.
|
|
24
36
|
--with-asset-files Downloads all files (assets) of the space. Defaults to false.
|
|
25
37
|
--output-dir <dir> Directory to write the backup to. Defaults to ./.output
|
|
26
38
|
(ATTENTION: Will fail if the directory already exists!)
|
|
@@ -38,8 +50,9 @@ MAXIMAL EXAMPLE
|
|
|
38
50
|
$ npx storyblok-backup \\
|
|
39
51
|
--token 1234567890abcdef \\
|
|
40
52
|
--space 12345 \\
|
|
53
|
+
--region ap \\
|
|
41
54
|
--with-asset-files \\
|
|
42
|
-
--output-dir ./
|
|
55
|
+
--output-dir ./my-dir \\
|
|
43
56
|
--force \\
|
|
44
57
|
--create-zip \\
|
|
45
58
|
--zip-prefix daily \\
|
|
@@ -48,17 +61,31 @@ MAXIMAL EXAMPLE
|
|
|
48
61
|
process.exit(0)
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
if (!('token' in args)) {
|
|
64
|
+
if (!('token' in args) && !process.env.STORYBLOK_OAUTH_TOKEN) {
|
|
52
65
|
console.log(
|
|
53
|
-
'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.'
|
|
54
67
|
)
|
|
55
68
|
process.exit(1)
|
|
56
69
|
}
|
|
70
|
+
const oauthToken = args.token || process.env.STORYBLOK_OAUTH_TOKEN
|
|
57
71
|
|
|
58
|
-
if (!('space' in args)) {
|
|
59
|
-
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
|
+
)
|
|
60
76
|
process.exit(1)
|
|
61
77
|
}
|
|
78
|
+
const spaceId = args.space || process.env.STORYBLOK_SPACE_ID
|
|
79
|
+
|
|
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
|
+
}
|
|
88
|
+
}
|
|
62
89
|
|
|
63
90
|
const verbose = 'verbose' in args
|
|
64
91
|
|
|
@@ -73,8 +100,6 @@ if (fs.existsSync(outputDir) && !('force' in args)) {
|
|
|
73
100
|
process.exit(1)
|
|
74
101
|
}
|
|
75
102
|
|
|
76
|
-
const spaceId = args.space
|
|
77
|
-
|
|
78
103
|
const filePrefix = args['zip-prefix'] || 'backup'
|
|
79
104
|
|
|
80
105
|
const fileName =
|
|
@@ -98,7 +123,8 @@ if ('create-zip' in args) {
|
|
|
98
123
|
|
|
99
124
|
// Init Management API
|
|
100
125
|
const StoryblokMAPI = new StoryblokClient({
|
|
101
|
-
oauthToken:
|
|
126
|
+
oauthToken: oauthToken,
|
|
127
|
+
region: region,
|
|
102
128
|
})
|
|
103
129
|
|
|
104
130
|
// Remove existing output directory
|
|
@@ -111,20 +137,25 @@ fs.mkdirSync(backupDir, { recursive: true })
|
|
|
111
137
|
|
|
112
138
|
const resources = [
|
|
113
139
|
'stories',
|
|
140
|
+
'collaborators',
|
|
114
141
|
'components',
|
|
115
142
|
'component-groups',
|
|
116
143
|
'assets',
|
|
117
144
|
'asset-folders',
|
|
145
|
+
'internal-tags',
|
|
118
146
|
'datasources',
|
|
119
147
|
'space-roles',
|
|
120
148
|
'tasks',
|
|
121
149
|
'activities',
|
|
122
150
|
'presets',
|
|
123
151
|
'field-types',
|
|
152
|
+
'webhooks',
|
|
124
153
|
'workflow-stages',
|
|
125
154
|
'workflow-stage-changes',
|
|
126
155
|
'workflows',
|
|
127
156
|
'releases',
|
|
157
|
+
'pipeline-branches',
|
|
158
|
+
'access-tokens',
|
|
128
159
|
]
|
|
129
160
|
resources.forEach((resource) => fs.mkdirSync(`${backupDir}/${resource}`))
|
|
130
161
|
|
|
@@ -155,7 +186,7 @@ const writeJson = (folder, file, content) => {
|
|
|
155
186
|
outputFile += `/${folder}`
|
|
156
187
|
}
|
|
157
188
|
outputFile += `/${file}.json`
|
|
158
|
-
fs.
|
|
189
|
+
fs.writeFileSync(outputFile, JSON.stringify(content, null, 2), (error) => {
|
|
159
190
|
if (error) {
|
|
160
191
|
throw error
|
|
161
192
|
}
|
|
@@ -188,7 +219,10 @@ await StoryblokMAPI.getAll(`spaces/${spaceId}/stories`)
|
|
|
188
219
|
.then(async (stories) => {
|
|
189
220
|
for (const story of stories) {
|
|
190
221
|
await StoryblokMAPI.get(`spaces/${spaceId}/stories/${story.id}`)
|
|
191
|
-
.then((response) =>
|
|
222
|
+
.then((response) => {
|
|
223
|
+
delete response.data.story.preview_token
|
|
224
|
+
writeJson('stories', story.id, response.data.story)
|
|
225
|
+
})
|
|
192
226
|
.catch((error) => {
|
|
193
227
|
throw error
|
|
194
228
|
})
|
|
@@ -198,6 +232,10 @@ await StoryblokMAPI.getAll(`spaces/${spaceId}/stories`)
|
|
|
198
232
|
throw error
|
|
199
233
|
})
|
|
200
234
|
|
|
235
|
+
// Fetch all collaborators
|
|
236
|
+
console.log(`Fetching collaborators`)
|
|
237
|
+
await defaultFetch('collaborators', 'collaborators', 'user_id')
|
|
238
|
+
|
|
201
239
|
// Fetch all components
|
|
202
240
|
console.log(`Fetching components`)
|
|
203
241
|
await defaultFetch('components', 'components', 'name')
|
|
@@ -227,6 +265,10 @@ await StoryblokMAPI.getAll(`spaces/${spaceId}/assets`)
|
|
|
227
265
|
console.log(`Fetching asset-folders`)
|
|
228
266
|
await defaultFetch('asset_folders', 'asset-folders', 'id')
|
|
229
267
|
|
|
268
|
+
// Fetch all internal-tags
|
|
269
|
+
console.log(`Fetching internal-tags`)
|
|
270
|
+
await defaultFetch('internal_tags', 'internal-tags', 'id')
|
|
271
|
+
|
|
230
272
|
// Fetch all datasources (including entries)
|
|
231
273
|
console.log(`Fetching datasources`)
|
|
232
274
|
await StoryblokMAPI.getAll(`spaces/${spaceId}/datasources`)
|
|
@@ -268,6 +310,10 @@ await defaultFetch('presets', 'presets', 'id')
|
|
|
268
310
|
console.log(`Fetching field-types`)
|
|
269
311
|
await defaultFetch('field_types', 'field-types', 'name')
|
|
270
312
|
|
|
313
|
+
// Fetch all webhooks
|
|
314
|
+
console.log(`Fetching webhooks`)
|
|
315
|
+
await defaultFetch('webhook_endpoints', 'webhooks', 'id')
|
|
316
|
+
|
|
271
317
|
// Fetch all workflow-stages
|
|
272
318
|
console.log(`Fetching workflow-stages`)
|
|
273
319
|
await defaultFetch('workflow_stages', 'workflow-stages', 'id')
|
|
@@ -284,6 +330,14 @@ await defaultFetch('workflows', 'workflows', 'id')
|
|
|
284
330
|
console.log(`Fetching releases`)
|
|
285
331
|
await defaultFetch('releases', 'releases', 'id')
|
|
286
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
|
+
|
|
287
341
|
// Create zip file
|
|
288
342
|
if ('create-zip' in args) {
|
|
289
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",
|
|
@@ -35,14 +36,14 @@
|
|
|
35
36
|
"homepage": "https://github.com/webflorist/storyblok-backup#readme",
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"eslint": "^8.49.0",
|
|
38
|
-
"eslint-config-prettier": "^9.
|
|
39
|
-
"eslint-plugin-prettier": "^5.
|
|
40
|
-
"prettier": "^3.
|
|
39
|
+
"eslint-config-prettier": "^9.1.0",
|
|
40
|
+
"eslint-plugin-prettier": "^5.1.3",
|
|
41
|
+
"prettier": "^3.3.2"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
|
-
"
|
|
44
|
+
"@dotenvx/dotenvx": "^1.6.2",
|
|
44
45
|
"minimist": "^1.2.8",
|
|
45
|
-
"storyblok-js-client": "^6.
|
|
46
|
-
"zip-lib": "^0.
|
|
46
|
+
"storyblok-js-client": "^6.7.3",
|
|
47
|
+
"zip-lib": "^1.0.4"
|
|
47
48
|
}
|
|
48
49
|
}
|