wikijs-fork/client/components/admin/admin-utilities-export.vue
Nicolas Giard cd33ff0afb
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
2022-05-16 01:13:42 -04:00

273 lines
7.6 KiB
Vue

<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>