mirror of https://github.com/requarks/wiki
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 assetspull/5304/head v2.5.282
parent
a37d733523
commit
cd33ff0afb
@ -0,0 +1,272 @@
|
||||
<template lang='pug'>
|
||||
v-card
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subtitle-1 {{ $t('admin:utilities.exportTitle') }}
|
||||
v-card-text
|
||||
.text-center
|
||||
img.animated.fadeInUp.wait-p1s(src='/_assets/svg/icon-big-parcel.svg')
|
||||
.body-2 Export to tarball / file system
|
||||
v-divider.my-4
|
||||
.body-2 What do you want to export?
|
||||
v-checkbox(
|
||||
v-for='choice of entityChoices'
|
||||
:key='choice.key'
|
||||
:label='choice.label'
|
||||
:value='choice.key'
|
||||
color='deep-orange darken-2'
|
||||
hide-details
|
||||
v-model='entities'
|
||||
)
|
||||
template(v-slot:label)
|
||||
div
|
||||
strong.deep-orange--text.text--darken-2 {{choice.label}}
|
||||
.text-caption {{choice.hint}}
|
||||
v-text-field.mt-7(
|
||||
outlined
|
||||
label='Target Folder Path'
|
||||
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!'
|
||||
persistent-hint
|
||||
v-model='filePath'
|
||||
)
|
||||
|
||||
v-alert.mt-3(color='deep-orange', outlined, icon='mdi-alert', prominent)
|
||||
.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.
|
||||
.body-2 For example, you may want to encrypt the archive if stored for backup purposes.
|
||||
|
||||
v-card-chin
|
||||
v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='entities.length < 1', @click='startExport').ml-0
|
||||
v-icon(left, color='white') mdi-database-export
|
||||
span.white--text Start Export
|
||||
v-dialog(
|
||||
v-model='isLoading'
|
||||
persistent
|
||||
max-width='350'
|
||||
)
|
||||
v-card(color='deep-orange darken-2', dark)
|
||||
v-card-text.pa-10.text-center
|
||||
self-building-square-spinner.animated.fadeIn(
|
||||
:animation-duration='4500'
|
||||
:size='40'
|
||||
color='#FFF'
|
||||
style='margin: 0 auto;'
|
||||
)
|
||||
.mt-5.body-1.white--text Exporting...
|
||||
.caption Please wait, this may take a while
|
||||
v-progress-linear.mt-5(
|
||||
color='white'
|
||||
:value='progress'
|
||||
stream
|
||||
rounded
|
||||
:buffer-value='0'
|
||||
)
|
||||
v-dialog(
|
||||
v-model='isSuccess'
|
||||
persistent
|
||||
max-width='350'
|
||||
)
|
||||
v-card(color='green darken-2', dark)
|
||||
v-card-text.pa-10.text-center
|
||||
v-icon(size='60') mdi-check-circle-outline
|
||||
.my-5.body-1.white--text Export completed
|
||||
v-card-actions.green.darken-1
|
||||
v-spacer
|
||||
v-btn.px-5(
|
||||
color='white'
|
||||
outlined
|
||||
@click='isSuccess = false'
|
||||
) Close
|
||||
v-spacer
|
||||
v-dialog(
|
||||
v-model='isFailed'
|
||||
persistent
|
||||
max-width='800'
|
||||
)
|
||||
v-card(color='red darken-2', dark)
|
||||
v-toolbar(color='red darken-2', dense)
|
||||
v-icon mdi-alert
|
||||
.body-2.pl-3 Export failed
|
||||
v-spacer
|
||||
v-btn.px-5(
|
||||
color='white'
|
||||
text
|
||||
@click='isFailed = false'
|
||||
) Close
|
||||
v-card-text.pa-5.red.darken-4.white--text
|
||||
span {{errorMessage}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { SelfBuildingSquareSpinner } from 'epic-spinners'
|
||||
|
||||
import gql from 'graphql-tag'
|
||||
import _get from 'lodash/get'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SelfBuildingSquareSpinner
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
entities: [],
|
||||
filePath: './data/export',
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isFailed: false,
|
||||
errorMessage: '',
|
||||
progress: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
entityChoices () {
|
||||
return [
|
||||
{
|
||||
key: 'assets',
|
||||
label: 'Assets',
|
||||
hint: 'Media files such as images, documents, etc.'
|
||||
},
|
||||
{
|
||||
key: 'comments',
|
||||
label: 'Comments',
|
||||
hint: 'Comments made using the default comment module only.'
|
||||
},
|
||||
{
|
||||
key: 'navigation',
|
||||
label: 'Navigation',
|
||||
hint: 'Sidebar links when using Static or Custom Navigation.'
|
||||
},
|
||||
{
|
||||
key: 'pages',
|
||||
label: 'Pages',
|
||||
hint: 'Page content, tags and related metadata.'
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
label: 'Pages History',
|
||||
hint: 'All previous versions of pages and their related metadata.'
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
label: 'Settings',
|
||||
hint: 'Site configuration and modules settings.'
|
||||
},
|
||||
{
|
||||
key: 'groups',
|
||||
label: 'User Groups',
|
||||
hint: 'Group permissions and page rules.'
|
||||
},
|
||||
{
|
||||
key: 'users',
|
||||
label: 'Users',
|
||||
hint: 'Users metadata and their group memberships.'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async checkProgress () {
|
||||
try {
|
||||
const respStatus = await this.$apollo.query({
|
||||
query: gql`
|
||||
{
|
||||
system {
|
||||
exportStatus {
|
||||
status
|
||||
progress
|
||||
message
|
||||
startedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
const respStatusObj = _get(respStatus, 'data.system.exportStatus', {})
|
||||
if (!respStatusObj) {
|
||||
throw new Error('An unexpected error occured.')
|
||||
} else {
|
||||
switch (respStatusObj.status) {
|
||||
case 'error': {
|
||||
throw new Error(respStatusObj.message || 'An unexpected error occured.')
|
||||
}
|
||||
case 'running': {
|
||||
this.progress = respStatusObj.progress || 0
|
||||
window.requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
this.checkProgress()
|
||||
}, 5000)
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'success': {
|
||||
this.isLoading = false
|
||||
this.isSuccess = true
|
||||
break
|
||||
}
|
||||
default: {
|
||||
throw new Error('Invalid export status.')
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.errorMessage = err.message
|
||||
this.isLoading = false
|
||||
this.isFailed = true
|
||||
}
|
||||
},
|
||||
async startExport () {
|
||||
this.isFailed = false
|
||||
this.isSuccess = false
|
||||
this.isLoading = true
|
||||
this.progress = 0
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// -> Initiate export
|
||||
const respExport = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation (
|
||||
$entities: [String]!
|
||||
$path: String!
|
||||
) {
|
||||
system {
|
||||
export (
|
||||
entities: $entities
|
||||
path: $path
|
||||
) {
|
||||
responseResult {
|
||||
succeeded
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
entities: this.entities,
|
||||
path: this.filePath
|
||||
}
|
||||
})
|
||||
|
||||
const respExportObj = _get(respExport, 'data.system.export', {})
|
||||
if (!_get(respExportObj, 'responseResult.succeeded', false)) {
|
||||
this.errorMessage = _get(respExportObj, 'responseResult.message', 'An unexpected error occurred')
|
||||
this.isLoading = false
|
||||
this.isFailed = true
|
||||
return
|
||||
}
|
||||
|
||||
// -> Check for progress
|
||||
this.checkProgress()
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
this.isLoading = false
|
||||
}
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
After Width: | Height: | Size: 2.5 KiB |
Loading…
Reference in new issue