admin-utilities-export.vue 7.6 KB

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