storyblok-backup 0.0.1

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/.editorconfig ADDED
@@ -0,0 +1,13 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ indent_style = tabs
8
+ indent_size = tab
9
+ tab_width = 4
10
+ trim_trailing_whitespace = true
11
+
12
+ [*.md]
13
+ trim_trailing_whitespace = false
package/.eslintrc.cjs ADDED
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ root: true,
3
+ env: {
4
+ node: true,
5
+ },
6
+ parserOptions: {
7
+ sourceType: 'module',
8
+ ecmaVersion: 'latest',
9
+ },
10
+ extends: ['plugin:prettier/recommended'],
11
+ }
package/.lintignore ADDED
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ pnpm-lock.yaml
package/.prettierrc.js ADDED
@@ -0,0 +1,16 @@
1
+ module.exports = {
2
+ semi: false,
3
+ singleQuote: true,
4
+ useTabs: true,
5
+ tabWidth: 4,
6
+ printWidth: 100,
7
+ trailingComma: 'es5',
8
+ overrides: [
9
+ {
10
+ files: ['**/*.md', '**/*.yaml', '**/*.yml'],
11
+ options: {
12
+ tabWidth: 2,
13
+ },
14
+ },
15
+ ],
16
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
3
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "editor.tabSize": 3,
3
+ "editor.insertSpaces": true,
4
+ "editor.detectIndentation": true,
5
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
6
+ "eslint.enable": true,
7
+ "editor.quickSuggestions": {
8
+ "strings": true
9
+ }
10
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 webflorist
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # Storyblok Backup CLI
2
+
3
+ [![npm version](https://img.shields.io/npm/v/storyblok-backup.svg)](https://www.npmjs.com/package/storyblok-backup)
4
+ [![license](https://img.shields.io/github/license/webflorist/storyblok-backup)](https://github.com/webflorist/storyblok-backup/blob/main/LICENSE)
5
+
6
+ A npx CLI tool to create a full backup of a space of the [Storyblok CMS](https://www.storyblok.com).
7
+
8
+ The script will fetch the following resources of a Storyblok space using the Management API and archive them in a zip file:
9
+
10
+ - Stories
11
+ - Components
12
+ - Component groups
13
+ - Assets (optionally incl. original files)
14
+ - Asset folders
15
+ - Datasources (incl. entries)
16
+ - Space
17
+ - Space Roles
18
+ - Tasks
19
+ - Activities
20
+ - Presets
21
+ - Field types
22
+ - Workflow stages
23
+ - Workflow stage changes
24
+ - Custom workflows
25
+ - Releases
26
+
27
+ ## Installation
28
+
29
+ ```shell
30
+ # install globally
31
+ $ npm install -g storyblok-backup
32
+
33
+ # or simply run via npx
34
+ $ npx storyblok-backup
35
+
36
+ # or install for project using npm
37
+ $ npm install storyblok-backup
38
+
39
+ # or install for project using yarn
40
+ $ yarn add storyblok-backup
41
+
42
+ # or install for project using pnpm
43
+ $ pnpm add storyblok-backup
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ### Options
49
+
50
+ ```text
51
+ --token <token> (required) Personal OAuth access token created
52
+ in the account settings of a Stoyblok user.
53
+ (NOT the Access Token of a Space!)
54
+ --space <space_id> (required) ID of the space to backup
55
+ --with-asset-files Downloads all files (assets) of the space. Defaults to false.
56
+ --output-dir <dir> Directory to write the backup to. Defaults to ./.output
57
+ (ATTENTION: Will fail if the directory already exists!)
58
+ --force Force deletion and recreation of existing output directory.
59
+ --create-zip Create a zip file of the backup. Defaults to false.
60
+ --zip-prefix <dir> Prefix for the zip file. Defaults to 'backup'.
61
+ (The suffix will automatically be the current date.)
62
+ --verbose Will show detailed output for every file written.
63
+ --help Show this help
64
+ ```
65
+
66
+ ### Minimal example
67
+
68
+ ```shell
69
+ npx storyblok-backup --token 1234567890abcdef --space 12345
70
+ ```
71
+
72
+ This will create the folder `./.output` and fetch all resources sorted into folders.
73
+
74
+ ### Maximal example
75
+
76
+ ```shell
77
+ npx storyblok-backup \
78
+ --token 1234567890abcdef \
79
+ --space 12345 \
80
+ --with-asset-files \
81
+ --output-dir ./my-dir \
82
+ --create-zip \
83
+ --zip-prefix daily \
84
+ --verbose
85
+ ```
86
+
87
+ This will create the folder `./my-dir`, 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.
88
+
89
+ ## Continuous Integration
90
+
91
+ You can e.g. use this script to create periodic backups of Storyblok spaces using GitHub Actions and artifacts.
92
+
93
+ Here would be an example for a weekly backup, that removes the artifacts/backups from previous runs and uploads a new one:
94
+
95
+ ```yaml
96
+ name: Weekly Storyblok Backup
97
+
98
+ on:
99
+ schedule:
100
+ - cron: '0 0 * * 0'
101
+
102
+ jobs:
103
+ build:
104
+ runs-on: ubuntu-latest
105
+
106
+ steps:
107
+ - name: Perform Backup
108
+ env:
109
+ STORYBLOK_OAUTH_TOKEN: ${{ secrets.STORYBLOK_OAUTH_TOKEN }}
110
+ STORYBLOK_SPACE_ID: ${{ secrets.STORYBLOK_SPACE_ID }}
111
+ run: npx storyblok-backup --token $STORYBLOK_OAUTH_TOKEN --space $STORYBLOK_SPACE_ID --create-zip
112
+
113
+ - name: Delete Old Artifacts
114
+ uses: actions/github-script@v6
115
+ id: artifact
116
+ with:
117
+ script: |
118
+ const res = await github.rest.actions.listArtifactsForRepo({
119
+ owner: context.repo.owner,
120
+ repo: context.repo.repo,
121
+ })
122
+
123
+ res.data.artifacts
124
+ .filter(({ name }) => name === 'weekly-backup')
125
+ .forEach(({ id }) => {
126
+ github.rest.actions.deleteArtifact({
127
+ owner: context.repo.owner,
128
+ repo: context.repo.repo,
129
+ artifact_id: id,
130
+ })
131
+ })
132
+
133
+ - name: Copy Artifact
134
+ run: mkdir artifact && cp ./.output/*.zip artifact
135
+
136
+ - name: Upload Artifact
137
+ uses: actions/upload-artifact@v3
138
+ with:
139
+ name: weekly-backup
140
+ path: artifact
141
+ ```
142
+
143
+ Make sure, to set the secrets `STORYBLOK_OAUTH_TOKEN` and `STORYBLOK_SPACE_ID` in your repository settings.
144
+
145
+ If you create multiple workflows for daily, weekly and monthly backups, by changing the cron-schedule and the two occurrences of the artifact name `weekly-backup`, you will always have exactly one daily, weekly and monthly backup.
146
+
147
+ 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)).
148
+
149
+ ## License
150
+
151
+ This package is open-sourced software licensed under the [MIT license](https://github.com/webflorist/storyblok-backup/blob/main/LICENSE.).
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ import fs from 'fs'
4
+ import { Readable } from 'stream'
5
+ import { finished } from 'stream/promises'
6
+ import zipLib from 'zip-lib'
7
+ import minimist from 'minimist'
8
+ import StoryblokClient from 'storyblok-js-client'
9
+ import { performance } from 'perf_hooks'
10
+
11
+ const startTime = performance.now()
12
+
13
+ const args = minimist(process.argv.slice(2))
14
+
15
+ if ('help' in args) {
16
+ console.log(`USAGE
17
+ $ npx storyblok-backup
18
+
19
+ OPTIONS
20
+ --token <token> (required) Personal OAuth access token created
21
+ in the account settings of a Stoyblok user.
22
+ (NOT the Access Token of a Space!)
23
+ --space <space_id> (required) ID of the space to backup
24
+ --with-asset-files Downloads all files (assets) of the space. Defaults to false.
25
+ --output-dir <dir> Directory to write the backup to. Defaults to ./.output
26
+ (ATTENTION: Will fail if the directory already exists!)
27
+ --force Force deletion and recreation of existing output directory.
28
+ --create-zip Create a zip file of the backup. Defaults to false.
29
+ --zip-prefix <dir> Prefix for the zip file. Defaults to 'backup'.
30
+ (The suffix will automatically be the current date.)
31
+ --verbose Will show detailed output for every file written.
32
+ --help Show this help
33
+
34
+ MINIMAL EXAMPLE
35
+ $ npx storyblok-backup --token 1234567890abcdef --space 12345
36
+
37
+ MAXIMAL EXAMPLE
38
+ $ npx storyblok-backup \\
39
+ --token 1234567890abcdef \\
40
+ --space 12345 \\
41
+ --output-dir ./backup \\
42
+ --zip-prefix daily
43
+ --verbose
44
+ `)
45
+ process.exit(0)
46
+ }
47
+
48
+ if (!('token' in args)) {
49
+ console.log(
50
+ 'Error: State your oauth token via the --token argument. Use --help to find out more.'
51
+ )
52
+ process.exit(1)
53
+ }
54
+
55
+ if (!('space' in args)) {
56
+ console.log('Error: State your space id via the --space argument. Use --help to find out more.')
57
+ process.exit(1)
58
+ }
59
+
60
+ const verbose = 'verbose' in args
61
+
62
+ const outputDir = args['output-dir'] || './.output'
63
+
64
+ if (fs.existsSync(outputDir) && !('force' in args)) {
65
+ console.log(
66
+ `Error: Output directory "${outputDir}" already exists. Use --force to delete and recreate it (POSSIBLY DANGEROUS!).`
67
+ )
68
+ process.exit(1)
69
+ }
70
+
71
+ const spaceId = args.space
72
+
73
+ const filePrefix = args['zip-prefix'] || 'backup'
74
+
75
+ const fileName =
76
+ [
77
+ filePrefix,
78
+ new Date().getFullYear(),
79
+ new Date().getMonth().toString().padStart(2, '0'),
80
+ new Date().getDay().toString().padStart(2, '0'),
81
+ new Date().getHours().toString().padStart(2, '0'),
82
+ new Date().getMinutes().toString().padStart(2, '0'),
83
+ new Date().getSeconds().toString().padStart(2, '0'),
84
+ ].join('-') + '.zip'
85
+
86
+ const filePath = `${outputDir}/${fileName}`
87
+
88
+ console.log(`Creating backup for space ${spaceId}:`)
89
+ console.log(`Output dir: ${outputDir}`)
90
+ if ('create-zip' in args) {
91
+ console.log(`Output zip: ${filePath}`)
92
+ }
93
+
94
+ // Init Management API
95
+ const StoryblokMAPI = new StoryblokClient({
96
+ oauthToken: args.token,
97
+ })
98
+
99
+ // Remove existing output directory
100
+ if (fs.existsSync(outputDir)) {
101
+ fs.rmSync(outputDir, { recursive: true, force: true })
102
+ }
103
+
104
+ // Create output directories
105
+ fs.mkdirSync(outputDir, { recursive: true })
106
+
107
+ const resources = [
108
+ 'stories',
109
+ 'components',
110
+ 'component-groups',
111
+ 'assets',
112
+ 'asset-folders',
113
+ 'datasources',
114
+ 'space-roles',
115
+ 'tasks',
116
+ 'activities',
117
+ 'presets',
118
+ 'field-types',
119
+ 'workflow-stages',
120
+ 'workflow-stage-changes',
121
+ 'workflows',
122
+ 'releases',
123
+ ]
124
+ resources.forEach((resource) => fs.mkdirSync(`${outputDir}/${resource}`))
125
+
126
+ // Function to perform a default fetch
127
+ const defaultFetch = async (type, folder, fileField) => {
128
+ await StoryblokMAPI.getAll(`spaces/${spaceId}/${type}`)
129
+ .then((items) => {
130
+ items.forEach((item) => writeJson(folder, item[fileField], item))
131
+ })
132
+ .catch((error) => {
133
+ throw error
134
+ })
135
+ }
136
+
137
+ // Function to write a file
138
+ const writeJson = (folder, file, content) => {
139
+ let outputFile = outputDir
140
+ if (folder !== null) {
141
+ outputFile += `/${folder}`
142
+ }
143
+ outputFile += `/${file}.json`
144
+ fs.writeFile(outputFile, JSON.stringify(content), (error) => {
145
+ if (error) {
146
+ throw error
147
+ }
148
+ })
149
+ if (verbose) console.log(`Written file ${outputFile}`)
150
+ }
151
+
152
+ // Function to download a file
153
+ const downloadFile = async (type, name, url) => {
154
+ const res = await fetch(url)
155
+ const outputFile = `${outputDir}/${type}/${name}`
156
+ const fileStream = fs.createWriteStream(outputFile, { flags: 'wx' })
157
+ await finished(Readable.fromWeb(res.body).pipe(fileStream))
158
+ if (verbose) console.log(`Written file ${outputFile}`)
159
+ }
160
+
161
+ // Fetch space info
162
+ console.log(`Fetching space`)
163
+ await StoryblokMAPI.get(`spaces/${spaceId}/`)
164
+ .then((space) => {
165
+ writeJson(null, `space-${spaceId}`, space.data.space)
166
+ })
167
+ .catch((error) => {
168
+ throw error
169
+ })
170
+
171
+ // Fetch all stories
172
+ console.log(`Fetching stories`)
173
+ await StoryblokMAPI.getAll(`spaces/${spaceId}/stories`)
174
+ .then(async (stories) => {
175
+ for (const story of stories) {
176
+ await StoryblokMAPI.get(`spaces/${spaceId}/stories/${story.id}`)
177
+ .then((response) => writeJson('stories', story.id, response.data.story))
178
+ .catch((error) => {
179
+ throw error
180
+ })
181
+ }
182
+ })
183
+ .catch((error) => {
184
+ throw error
185
+ })
186
+
187
+ // Fetch all components
188
+ console.log(`Fetching components`)
189
+ await defaultFetch('components', 'components', 'name')
190
+
191
+ // Fetch all component-groups
192
+ console.log(`Fetching component-groups`)
193
+ await defaultFetch('component_groups', 'component-groups', 'id')
194
+
195
+ // Fetch all assets (including files)
196
+ console.log(`Fetching assets`)
197
+ await StoryblokMAPI.getAll(`spaces/${spaceId}/assets`)
198
+ .then(async (assets) => {
199
+ for (const asset of assets) {
200
+ writeJson('assets', asset.id, asset)
201
+ if ('with-asset-files' in args) {
202
+ const fileExtension = asset.filename.split('.').at(-1)
203
+ const fileName = asset.id + '.' + fileExtension
204
+ await downloadFile('assets', fileName, asset.filename)
205
+ }
206
+ }
207
+ })
208
+ .catch((error) => {
209
+ throw error
210
+ })
211
+
212
+ // Fetch all asset-folders
213
+ console.log(`Fetching asset-folders`)
214
+ await defaultFetch('asset_folders', 'asset-folders', 'id')
215
+
216
+ // Fetch all datasources (including entries)
217
+ console.log(`Fetching datasources`)
218
+ await StoryblokMAPI.getAll(`spaces/${spaceId}/datasources`)
219
+ .then(async (datasources) => {
220
+ for (const datasource of datasources) {
221
+ writeJson('datasources', datasource.id, datasource)
222
+ await StoryblokMAPI.getAll(`spaces/${spaceId}/datasource_entries`, {
223
+ datasource_id: datasource.id,
224
+ })
225
+ .then((dateSourceEntries) =>
226
+ writeJson('datasources', datasource.id + '_entries', dateSourceEntries)
227
+ )
228
+ .catch((error) => {
229
+ throw error
230
+ })
231
+ }
232
+ })
233
+ .catch((error) => {
234
+ throw error
235
+ })
236
+
237
+ // Fetch all space roles
238
+ console.log(`Fetching space roles`)
239
+ await defaultFetch('space_roles', 'space-roles', 'id')
240
+
241
+ // Fetch all tasks
242
+ console.log(`Fetching tasks`)
243
+ await defaultFetch('tasks', 'tasks', 'id')
244
+
245
+ // Fetch all activities
246
+ console.log(`Fetching activities`)
247
+ await defaultFetch('activities', 'activities', 'id')
248
+
249
+ // Fetch all presets
250
+ console.log(`Fetching presets`)
251
+ await defaultFetch('presets', 'presets', 'id')
252
+
253
+ // Fetch all field-types
254
+ console.log(`Fetching field-types`)
255
+ await defaultFetch('field_types', 'field-types', 'name')
256
+
257
+ // Fetch all workflow-stages
258
+ console.log(`Fetching workflow-stages`)
259
+ await defaultFetch('workflow_stages', 'workflow-stages', 'id')
260
+
261
+ // Fetch all workflow-stage-changes
262
+ console.log(`Fetching workflow-stage-changes`)
263
+ await defaultFetch('workflow_stage_changes', 'workflow-stage-changes', 'id')
264
+
265
+ // Fetch all workflows
266
+ console.log(`Fetching workflows`)
267
+ await defaultFetch('workflows', 'workflows', 'id')
268
+
269
+ // Fetch all releases
270
+ console.log(`Fetching releases`)
271
+ await defaultFetch('releases', 'releases', 'id')
272
+
273
+ // Create zip file
274
+ if ('create-zip' in args) {
275
+ console.log(`Creating zip file`)
276
+ await zipLib
277
+ .archiveFolder(outputDir, filePath)
278
+ .then(
279
+ function () {
280
+ console.log(`Backup file '${filePath}' successfully created.`)
281
+ },
282
+ function (err) {
283
+ throw err
284
+ }
285
+ )
286
+ .catch((error) => {
287
+ throw error
288
+ })
289
+ }
290
+
291
+ const endTime = performance.now()
292
+
293
+ console.log(`Backup successfully created in ${Math.round((endTime - startTime) / 1000)} seconds.`)
294
+ process.exit(0)
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "storyblok-backup",
3
+ "version": "0.0.1",
4
+ "description": "npx CLI tool to create a full backup of a Storyblok space",
5
+ "scripts": {
6
+ "upgrade": "npx npm-check-updates -i -u && pnpm install",
7
+ "lint:js": "eslint --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .lintignore .",
8
+ "lintfix:js": "pnpm lint:js --fix",
9
+ "lint:prettier": "prettier --ignore-path ./.lintignore --check .",
10
+ "lintfix:prettier": "prettier --ignore-path ./.lintignore --write --list-different .",
11
+ "lint": "pnpm lint:js && pnpm lint:prettier",
12
+ "lintfix": "pnpm lintfix:js && pnpm lintfix:prettier"
13
+ },
14
+ "bin": {
15
+ "storyblok-backup": "bin/storyblok-backup.mjs"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/webflorist/storyblok-backup.git"
20
+ },
21
+ "keywords": [
22
+ "storyblok",
23
+ "cms",
24
+ "backup",
25
+ "cli",
26
+ "node",
27
+ "script",
28
+ "npx"
29
+ ],
30
+ "author": "Gerald Buttinger <gerald@code.florist>",
31
+ "license": "MIT",
32
+ "bugs": {
33
+ "url": "https://github.com/webflorist/storyblok-backup/issues"
34
+ },
35
+ "homepage": "https://github.com/webflorist/storyblok-backup#readme",
36
+ "devDependencies": {
37
+ "eslint": "^8.49.0",
38
+ "eslint-config-prettier": "^9.0.0",
39
+ "eslint-plugin-prettier": "^5.0.0",
40
+ "prettier": "^3.0.3"
41
+ },
42
+ "dependencies": {
43
+ "archiver": "^6.0.1",
44
+ "minimist": "^1.2.8",
45
+ "storyblok-js-client": "^6.0.0",
46
+ "zip-lib": "^0.7.3"
47
+ }
48
+ }