Skip to content

Commit cd33ff0

Browse files
authored
feat(admin): export tool for full migration / backup (#5294)
* feat: export content utility (wip) * feat: export navigation + groups + users * feat: export comments + navigation + pages + pages history + settings * feat: export assets
1 parent a37d733 commit cd33ff0

File tree

9 files changed

+721
-5
lines changed

9 files changed

+721
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
<template lang='pug'>
2+
v-card
3+
v-toolbar(flat, color='primary', dark, dense)
4+
.subtitle-1 {{ $t('admin:utilities.exportTitle') }}
5+
v-card-text
6+
.text-center
7+
img.animated.fadeInUp.wait-p1s(src='/_assets/svg/icon-big-parcel.svg')
8+
.body-2 Export to tarball / file system
9+
v-divider.my-4
10+
.body-2 What do you want to export?
11+
v-checkbox(
12+
v-for='choice of entityChoices'
13+
:key='choice.key'
14+
:label='choice.label'
15+
:value='choice.key'
16+
color='deep-orange darken-2'
17+
hide-details
18+
v-model='entities'
19+
)
20+
template(v-slot:label)
21+
div
22+
strong.deep-orange--text.text--darken-2 {{choice.label}}
23+
.text-caption {{choice.hint}}
24+
v-text-field.mt-7(
25+
outlined
26+
label='Target Folder Path'
27+
hint='Either an absolute path or relative to the Wiki.js installation folder, where exported content will be saved to. Note that the folder MUST be empty!'
28+
persistent-hint
29+
v-model='filePath'
30+
)
31+
32+
v-alert.mt-3(color='deep-orange', outlined, icon='mdi-alert', prominent)
33+
.body-2 Depending on your selection, the archive could contain sensitive data such as site configuration keys and hashed user passwords. Ensure the exported archive is treated accordingly.
34+
.body-2 For example, you may want to encrypt the archive if stored for backup purposes.
35+
36+
v-card-chin
37+
v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='entities.length < 1', @click='startExport').ml-0
38+
v-icon(left, color='white') mdi-database-export
39+
span.white--text Start Export
40+
v-dialog(
41+
v-model='isLoading'
42+
persistent
43+
max-width='350'
44+
)
45+
v-card(color='deep-orange darken-2', dark)
46+
v-card-text.pa-10.text-center
47+
self-building-square-spinner.animated.fadeIn(
48+
:animation-duration='4500'
49+
:size='40'
50+
color='#FFF'
51+
style='margin: 0 auto;'
52+
)
53+
.mt-5.body-1.white--text Exporting...
54+
.caption Please wait, this may take a while
55+
v-progress-linear.mt-5(
56+
color='white'
57+
:value='progress'
58+
stream
59+
rounded
60+
:buffer-value='0'
61+
)
62+
v-dialog(
63+
v-model='isSuccess'
64+
persistent
65+
max-width='350'
66+
)
67+
v-card(color='green darken-2', dark)
68+
v-card-text.pa-10.text-center
69+
v-icon(size='60') mdi-check-circle-outline
70+
.my-5.body-1.white--text Export completed
71+
v-card-actions.green.darken-1
72+
v-spacer
73+
v-btn.px-5(
74+
color='white'
75+
outlined
76+
@click='isSuccess = false'
77+
) Close
78+
v-spacer
79+
v-dialog(
80+
v-model='isFailed'
81+
persistent
82+
max-width='800'
83+
)
84+
v-card(color='red darken-2', dark)
85+
v-toolbar(color='red darken-2', dense)
86+
v-icon mdi-alert
87+
.body-2.pl-3 Export failed
88+
v-spacer
89+
v-btn.px-5(
90+
color='white'
91+
text
92+
@click='isFailed = false'
93+
) Close
94+
v-card-text.pa-5.red.darken-4.white--text
95+
span {{errorMessage}}
96+
</template>
97+
98+
<script>
99+
import { SelfBuildingSquareSpinner } from 'epic-spinners'
100+
101+
import gql from 'graphql-tag'
102+
import _get from 'lodash/get'
103+
104+
export default {
105+
components: {
106+
SelfBuildingSquareSpinner
107+
},
108+
data() {
109+
return {
110+
entities: [],
111+
filePath: './data/export',
112+
isLoading: false,
113+
isSuccess: false,
114+
isFailed: false,
115+
errorMessage: '',
116+
progress: 0
117+
}
118+
},
119+
computed: {
120+
entityChoices () {
121+
return [
122+
{
123+
key: 'assets',
124+
label: 'Assets',
125+
hint: 'Media files such as images, documents, etc.'
126+
},
127+
{
128+
key: 'comments',
129+
label: 'Comments',
130+
hint: 'Comments made using the default comment module only.'
131+
},
132+
{
133+
key: 'navigation',
134+
label: 'Navigation',
135+
hint: 'Sidebar links when using Static or Custom Navigation.'
136+
},
137+
{
138+
key: 'pages',
139+
label: 'Pages',
140+
hint: 'Page content, tags and related metadata.'
141+
},
142+
{
143+
key: 'history',
144+
label: 'Pages History',
145+
hint: 'All previous versions of pages and their related metadata.'
146+
},
147+
{
148+
key: 'settings',
149+
label: 'Settings',
150+
hint: 'Site configuration and modules settings.'
151+
},
152+
{
153+
key: 'groups',
154+
label: 'User Groups',
155+
hint: 'Group permissions and page rules.'
156+
},
157+
{
158+
key: 'users',
159+
label: 'Users',
160+
hint: 'Users metadata and their group memberships.'
161+
}
162+
]
163+
}
164+
},
165+
methods: {
166+
async checkProgress () {
167+
try {
168+
const respStatus = await this.$apollo.query({
169+
query: gql`
170+
{
171+
system {
172+
exportStatus {
173+
status
174+
progress
175+
message
176+
startedAt
177+
}
178+
}
179+
}
180+
`,
181+
fetchPolicy: 'network-only'
182+
})
183+
const respStatusObj = _get(respStatus, 'data.system.exportStatus', {})
184+
if (!respStatusObj) {
185+
throw new Error('An unexpected error occured.')
186+
} else {
187+
switch (respStatusObj.status) {
188+
case 'error': {
189+
throw new Error(respStatusObj.message || 'An unexpected error occured.')
190+
}
191+
case 'running': {
192+
this.progress = respStatusObj.progress || 0
193+
window.requestAnimationFrame(() => {
194+
setTimeout(() => {
195+
this.checkProgress()
196+
}, 5000)
197+
})
198+
break
199+
}
200+
case 'success': {
201+
this.isLoading = false
202+
this.isSuccess = true
203+
break
204+
}
205+
default: {
206+
throw new Error('Invalid export status.')
207+
}
208+
}
209+
}
210+
} catch (err) {
211+
this.errorMessage = err.message
212+
this.isLoading = false
213+
this.isFailed = true
214+
}
215+
},
216+
async startExport () {
217+
this.isFailed = false
218+
this.isSuccess = false
219+
this.isLoading = true
220+
this.progress = 0
221+
222+
setTimeout(async () => {
223+
try {
224+
// -> Initiate export
225+
const respExport = await this.$apollo.mutate({
226+
mutation: gql`
227+
mutation (
228+
$entities: [String]!
229+
$path: String!
230+
) {
231+
system {
232+
export (
233+
entities: $entities
234+
path: $path
235+
) {
236+
responseResult {
237+
succeeded
238+
message
239+
}
240+
}
241+
}
242+
}
243+
`,
244+
variables: {
245+
entities: this.entities,
246+
path: this.filePath
247+
}
248+
})
249+
250+
const respExportObj = _get(respExport, 'data.system.export', {})
251+
if (!_get(respExportObj, 'responseResult.succeeded', false)) {
252+
this.errorMessage = _get(respExportObj, 'responseResult.message', 'An unexpected error occurred')
253+
this.isLoading = false
254+
this.isFailed = true
255+
return
256+
}
257+
258+
// -> Check for progress
259+
this.checkProgress()
260+
} catch (err) {
261+
this.$store.commit('pushGraphError', err)
262+
this.isLoading = false
263+
}
264+
}, 1500)
265+
}
266+
}
267+
}
268+
</script>
269+
270+
<style lang='scss'>
271+
272+
</style>

client/components/admin/admin-utilities.vue

+7
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export default {
3737
UtilityAuth: () => import(/* webpackChunkName: "admin" */ './admin-utilities-auth.vue'),
3838
UtilityContent: () => import(/* webpackChunkName: "admin" */ './admin-utilities-content.vue'),
3939
UtilityCache: () => import(/* webpackChunkName: "admin" */ './admin-utilities-cache.vue'),
40+
UtilityExport: () => import(/* webpackChunkName: "admin" */ './admin-utilities-export.vue'),
4041
UtilityImportv1: () => import(/* webpackChunkName: "admin" */ './admin-utilities-importv1.vue'),
4142
UtilityTelemetry: () => import(/* webpackChunkName: "admin" */ './admin-utilities-telemetry.vue')
4243
},
@@ -56,6 +57,12 @@ export default {
5657
i18nKey: 'content',
5758
isAvailable: true
5859
},
60+
{
61+
key: 'UtilityExport',
62+
icon: 'mdi-database-export',
63+
i18nKey: 'export',
64+
isAvailable: true
65+
},
5966
{
6067
key: 'UtilityCache',
6168
icon: 'mdi-database-refresh',

client/static/svg/icon-big-parcel.svg

+1
Loading

dev/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const init = {
6060
},
6161
async reload() {
6262
console.warn(chalk.yellow('--- Gracefully stopping server...'))
63-
await global.WIKI.kernel.shutdown()
63+
await global.WIKI.kernel.shutdown(true)
6464

6565
console.warn(chalk.yellow('--- Purging node modules cache...'))
6666

server/core/kernel.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ module.exports = {
106106
/**
107107
* Graceful shutdown
108108
*/
109-
async shutdown () {
109+
async shutdown (devMode = false) {
110110
if (WIKI.servers) {
111111
await WIKI.servers.stopServers()
112112
}
@@ -122,6 +122,8 @@ module.exports = {
122122
if (WIKI.asar) {
123123
await WIKI.asar.unload()
124124
}
125-
process.exit(0)
125+
if (!devMode) {
126+
process.exit(0)
127+
}
126128
}
127129
}

server/core/scheduler.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class Job {
6060
cwd: WIKI.ROOTPATH,
6161
stdio: ['inherit', 'inherit', 'pipe', 'ipc']
6262
})
63-
const stderr = [];
63+
const stderr = []
6464
proc.stderr.on('data', chunk => stderr.push(chunk))
6565
this.finished = new Promise((resolve, reject) => {
6666
proc.on('exit', (code, signal) => {

0 commit comments

Comments
 (0)