mirror of https://github.com/Requarks/wiki.git
Browse Source
feat(admin): export tool for full migration / backup (#5294)
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
Nicolas Giard
2 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 721 additions and 5 deletions
Split View
Diff Options
-
272client/components/admin/admin-utilities-export.vue
-
7client/components/admin/admin-utilities.vue
-
1client/static/svg/icon-big-parcel.svg
-
2dev/index.js
-
6server/core/kernel.js
-
2server/core/scheduler.js
-
380server/core/system.js
-
41server/graph/resolvers/system.js
-
15server/graph/schemas/system.graphql
@ -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> |
@ -0,0 +1 @@ |
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="144px" height="144px"><linearGradient id="rwH3R4FXAjAwf7QMo6soOa" x1="24.523" x2="39.672" y1="7.827" y2="22.933" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c26715"/><stop offset=".508" stop-color="#b85515"/><stop offset="1" stop-color="#ad3f16"/></linearGradient><path fill="url(#rwH3R4FXAjAwf7QMo6soOa)" d="M42,17H15V6h26c0.552,0,1,0.448,1,1V17z"/><linearGradient id="rwH3R4FXAjAwf7QMo6soOb" x1="7.292" x2="27.973" y1="1.98" y2="18.107" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#eba84b"/><stop offset="1" stop-color="#d97218"/></linearGradient><path fill="url(#rwH3R4FXAjAwf7QMo6soOb)" d="M32,17H7c-0.552,0-1-0.448-1-1V7c0-0.552,0.448-1,1-1h25c0.552,0,1,0.448,1,1v9 C33,16.552,32.552,17,32,17z"/><path d="M42,14H6v2c0,0.552,0.448,1,1,1h8h17h10V14z" opacity=".05"/><path d="M42,14.5H6V16c0,0.552,0.448,1,1,1h8h17h10V14.5z" opacity=".07"/><linearGradient id="rwH3R4FXAjAwf7QMo6soOc" x1="27.534" x2="46.45" y1="492.536" y2="512.013" gradientTransform="translate(0 -474)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#eba600"/><stop offset="1" stop-color="#c28200"/></linearGradient><path fill="url(#rwH3R4FXAjAwf7QMo6soOc)" d="M42,42H31V15h12c0.552,0,1,0.448,1,1v24C44,41.105,43.105,42,42,42z"/><linearGradient id="rwH3R4FXAjAwf7QMo6soOd" x1="5.418" x2="31.69" y1="488.435" y2="515.487" gradientTransform="translate(0 -474)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd869"/><stop offset="1" stop-color="#fec52b"/></linearGradient><path fill="url(#rwH3R4FXAjAwf7QMo6soOd)" d="M31,42H6c-1.105,0-2-0.895-2-2V16c0-0.552,0.448-1,1-1h28v25C33,41.105,32.105,42,31,42z"/><linearGradient id="rwH3R4FXAjAwf7QMo6soOe" x1="17.154" x2="17.154" y1="494.74" y2="463.029" gradientTransform="translate(0 -474)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#eba600"/><stop offset="1" stop-color="#c28200"/></linearGradient><path fill="url(#rwH3R4FXAjAwf7QMo6soOe)" d="M33,15H4.618c-0.379,0-0.725,0.214-0.894,0.553l-2.362,4.724C1.196,20.609,1.437,21,1.809,21 h27.573c0.379,0,0.725-0.214,0.894-0.553L33,15z"/><linearGradient id="rwH3R4FXAjAwf7QMo6soOf" x1="39.846" x2="39.846" y1="494.729" y2="490.572" gradientTransform="translate(0 -474)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd869"/><stop offset="1" stop-color="#fec52b"/></linearGradient><path fill="url(#rwH3R4FXAjAwf7QMo6soOf)" d="M33,15h10.382c0.379,0,0.725,0.214,0.894,0.553l2.362,4.724 C46.804,20.609,46.563,21,46.191,21h-9.573c-0.379,0-0.725-0.214-0.894-0.553L33,15z"/></svg> |
Write
Preview
Loading…
Cancel
Save