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