admin-utilities-importv1.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. <template lang='pug'>
  2. v-card
  3. v-toolbar(flat, color='primary', dark, dense)
  4. .subtitle-1 {{ $t('admin:utilities.importv1Title') }}
  5. v-card-text
  6. .text-center
  7. img.animated.fadeInUp.wait-p1s(src='/_assets/svg/icon-software.svg')
  8. .body-2 Import from Wiki.js 1.x
  9. v-divider.my-4
  10. .body-2 Data from a Wiki.js 1.x installation can easily be imported using this tool. What do you want to import?
  11. v-checkbox(
  12. label='Content + Uploads'
  13. value='content'
  14. color='deep-orange darken-2'
  15. v-model='importFilters'
  16. hide-details
  17. )
  18. template(v-slot:label)
  19. strong.deep-orange--text.text--darken-2 Content + Uploads
  20. .pl-8(v-if='wantContent')
  21. v-radio-group(v-model='contentMode', hide-details)
  22. v-radio(
  23. value='git'
  24. color='primary'
  25. )
  26. template(v-slot:label)
  27. div
  28. span Import from Git Connection
  29. .caption: em #[strong.primary--text Recommended] | The Git storage module will also be configured for you.
  30. .pl-8.mt-5(v-if='needGit')
  31. v-row
  32. v-col(cols='8')
  33. v-select(
  34. label='Authentication Mode'
  35. :items='gitAuthModes'
  36. v-model='gitAuthMode'
  37. outlined
  38. hide-details
  39. )
  40. v-col(cols='4')
  41. v-switch(
  42. label='Verify SSL Certificate'
  43. v-model='gitVerifySSL'
  44. hide-details
  45. color='primary'
  46. )
  47. v-col(cols='8')
  48. v-text-field(
  49. outlined
  50. label='Repository URL'
  51. :placeholder='(gitAuthMode === `ssh`) ? `e.g. git@github.com:orgname/repo.git` : `e.g. https://github.com/orgname/repo.git`'
  52. hide-details
  53. v-model='gitRepoUrl'
  54. )
  55. v-col(cols='4')
  56. v-text-field(
  57. label='Branch'
  58. placeholder='e.g. master'
  59. v-model='gitRepoBranch'
  60. outlined
  61. hide-details
  62. )
  63. v-col(v-if='gitAuthMode === `ssh`', cols='12')
  64. v-textarea(
  65. outlined
  66. label='Private Key Contents'
  67. placeholder='-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----'
  68. hide-details
  69. v-model='gitPrivKey'
  70. )
  71. template(v-else-if='gitAuthMode === `basic`')
  72. v-col(cols='6')
  73. v-text-field(
  74. label='Username'
  75. v-model='gitUsername'
  76. outlined
  77. hide-details
  78. )
  79. v-col(cols='6')
  80. v-text-field(
  81. type='password'
  82. label='Password / PAT'
  83. v-model='gitPassword'
  84. outlined
  85. hide-details
  86. )
  87. v-col(cols='6')
  88. v-text-field(
  89. label='Default Author Email'
  90. placeholder='e.g. name@company.com'
  91. v-model='gitUserEmail'
  92. outlined
  93. hide-details
  94. )
  95. v-col(cols='6')
  96. v-text-field(
  97. label='Default Author Name'
  98. placeholder='e.g. John Smith'
  99. v-model='gitUserName'
  100. outlined
  101. hide-details
  102. )
  103. v-col(cols='12')
  104. v-text-field(
  105. label='Local Repository Path'
  106. placeholder='e.g. ./data/repo'
  107. v-model='gitRepoPath'
  108. outlined
  109. hide-details
  110. )
  111. .caption.mt-2 This folder should be empty or not exist yet. #[strong.deep-orange--text.text--darken-2 DO NOT] point to your existing Wiki.js 1.x repository folder. In most cases, it should be left to the default value.
  112. v-alert(color='deep-orange', outlined, icon='mdi-alert', prominent)
  113. .body-2 - Note that if you already configured the git storage module, its configuration will be replaced with the above.
  114. .body-2 - Although both v1 and v2 installations can use the same remote git repository, you shouldn't make edits to the same pages simultaneously.
  115. v-radio-group(v-model='contentMode', hide-details)
  116. v-divider
  117. v-radio.mt-3(
  118. value='disk'
  119. color='primary'
  120. )
  121. template(v-slot:label)
  122. div
  123. span Import from local folder
  124. .caption: em Choose this option only if you didn't have git configured in your Wiki.js 1.x installation.
  125. .pl-8.mt-5(v-if='needDisk')
  126. v-text-field(
  127. outlined
  128. label='Content Repo Path'
  129. hint='The absolute path to where the Wiki.js 1.x content is stored on disk.'
  130. persistent-hint
  131. v-model='contentPath'
  132. )
  133. v-checkbox(
  134. label='Users'
  135. value='users'
  136. color='deep-orange darken-2'
  137. v-model='importFilters'
  138. hide-details
  139. )
  140. template(v-slot:label)
  141. strong.deep-orange--text.text--darken-2 Users
  142. .pl-8.mt-5(v-if='wantUsers')
  143. v-text-field(
  144. outlined
  145. label='MongoDB Connection String'
  146. hint='The connection string to connect to the Wiki.js 1.x MongoDB database.'
  147. persistent-hint
  148. v-model='dbConnStr'
  149. )
  150. v-radio-group(v-model='groupMode', hide-details, mandatory)
  151. v-radio(
  152. value='MULTI'
  153. color='primary'
  154. )
  155. template(v-slot:label)
  156. div
  157. span Create groups for each unique user permissions configuration
  158. .caption: em #[strong.primary--text Recommended] | Users having identical permission sets will be assigned to the same group. Note that this can potentially result in a large amount of groups being created.
  159. v-divider
  160. v-radio.mt-3(
  161. value='SINGLE'
  162. color='primary'
  163. )
  164. template(v-slot:label)
  165. div
  166. span Create a single group with all imported users
  167. .caption: em The new group will have read permissions enabled by default.
  168. v-divider
  169. v-radio.mt-3(
  170. value='NONE'
  171. color='primary'
  172. )
  173. template(v-slot:label)
  174. div
  175. span Don't create any group
  176. .caption: em Users will not be able to access your wiki until they are assigned to a group.
  177. v-alert.mt-5(color='deep-orange', outlined, icon='mdi-alert', prominent)
  178. .body-2 Note that any user that already exists in this installation will not be imported. A list of skipped users will be displayed upon completion.
  179. .caption.grey--text You must first delete from this installation any user you want to migrate over from the old installation.
  180. v-card-chin
  181. v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='!wantUsers && !wantContent', @click='startImport').ml-0
  182. v-icon(left, color='white') mdi-database-import
  183. span.white--text Start Import
  184. v-dialog(
  185. v-model='isLoading'
  186. persistent
  187. max-width='350'
  188. )
  189. v-card(color='deep-orange darken-2', dark)
  190. v-card-text.pa-10.text-center
  191. semipolar-spinner.animated.fadeIn(
  192. :animation-duration='1500'
  193. :size='65'
  194. color='#FFF'
  195. style='margin: 0 auto;'
  196. )
  197. .mt-5.body-1.white--text Importing from Wiki.js 1.x...
  198. .caption Please wait
  199. v-progress-linear.mt-5(
  200. color='white'
  201. :value='progress'
  202. stream
  203. rounded
  204. :buffer-value='0'
  205. )
  206. v-dialog(
  207. v-model='isSuccess'
  208. persistent
  209. max-width='350'
  210. )
  211. v-card(color='green darken-2', dark)
  212. v-card-text.pa-10.text-center
  213. v-icon(size='60') mdi-check-circle-outline
  214. .my-5.body-1.white--text Import completed
  215. template(v-if='wantUsers')
  216. .body-2
  217. span #[strong {{successUsers}}] users imported
  218. v-btn.text-none.ml-3(
  219. v-if='failedUsers.length > 0'
  220. text
  221. color='white'
  222. dark
  223. @click='showFailedUsers = true'
  224. )
  225. v-icon(left) mdi-alert
  226. span {{failedUsers.length}} failed
  227. .body-2 #[strong {{successGroups}}] groups created
  228. v-card-actions.green.darken-1
  229. v-spacer
  230. v-btn.px-5(
  231. color='white'
  232. outlined
  233. @click='isSuccess = false'
  234. ) Close
  235. v-spacer
  236. v-dialog(
  237. v-model='showFailedUsers'
  238. persistent
  239. max-width='800'
  240. )
  241. v-card(color='red darken-2', dark)
  242. v-toolbar(color='red darken-2', dense)
  243. v-icon mdi-alert
  244. .body-2.pl-3 Failed User Imports
  245. v-spacer
  246. v-btn.px-5(
  247. color='white'
  248. text
  249. @click='showFailedUsers = false'
  250. ) Close
  251. v-simple-table(dense, fixed-header, height='300px')
  252. template(v-slot:default)
  253. thead
  254. tr
  255. th Provider
  256. th Email
  257. th Error
  258. tbody
  259. tr(v-for='(fusr, idx) in failedUsers', :key='`fusr-` + idx')
  260. td {{fusr.provider}}
  261. td {{fusr.email}}
  262. td {{fusr.error}}
  263. </template>
  264. <script>
  265. import _ from 'lodash'
  266. import { SemipolarSpinner } from 'epic-spinners'
  267. import utilityImportv1UsersMutation from 'gql/admin/utilities/utilities-mutation-importv1-users.gql'
  268. import storageTargetsQuery from 'gql/admin/storage/storage-query-targets.gql'
  269. import storageStatusQuery from 'gql/admin/storage/storage-query-status.gql'
  270. import targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql'
  271. import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'
  272. export default {
  273. components: {
  274. SemipolarSpinner
  275. },
  276. data() {
  277. return {
  278. importFilters: ['content', 'users'],
  279. groupMode: 'MULTI',
  280. contentMode: 'git',
  281. dbConnStr: 'mongodb://',
  282. contentPath: '/wiki-v1/repo',
  283. isLoading: false,
  284. isSuccess: false,
  285. gitAuthMode: 'ssh',
  286. gitAuthModes: [
  287. { text: 'SSH', value: 'ssh' },
  288. { text: 'Basic', value: 'basic' }
  289. ],
  290. gitVerifySSL: true,
  291. gitRepoUrl: '',
  292. gitRepoBranch: 'master',
  293. gitPrivKey: '',
  294. gitUsername: '',
  295. gitPassword: '',
  296. gitUserEmail: '',
  297. gitUserName: '',
  298. gitRepoPath: './data/repo',
  299. progress: 0,
  300. successGroups: 0,
  301. successUsers: 0,
  302. successPages: 0,
  303. showFailedUsers: false,
  304. failedUsers: []
  305. }
  306. },
  307. computed: {
  308. wantContent () {
  309. return this.importFilters.indexOf('content') >= 0
  310. },
  311. wantUsers () {
  312. return this.importFilters.indexOf('users') >= 0
  313. },
  314. needDisk () {
  315. return this.contentMode === `disk`
  316. },
  317. needGit () {
  318. return this.contentMode === `git`
  319. }
  320. },
  321. methods: {
  322. async startImport () {
  323. this.isLoading = true
  324. this.progress = 0
  325. this.failedUsers = []
  326. _.delay(async () => {
  327. // -> Import Users
  328. if (this.wantUsers) {
  329. try {
  330. const resp = await this.$apollo.mutate({
  331. mutation: utilityImportv1UsersMutation,
  332. variables: {
  333. mongoDbConnString: this.dbConnStr,
  334. groupMode: this.groupMode
  335. }
  336. })
  337. const respObj = _.get(resp, 'data.system.importUsersFromV1', {})
  338. if (!_.get(respObj, 'responseResult.succeeded', false)) {
  339. throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occurred'))
  340. }
  341. this.successUsers = _.get(respObj, 'usersCount', 0)
  342. this.successGroups = _.get(respObj, 'groupsCount', 0)
  343. this.failedUsers = _.get(respObj, 'failed', [])
  344. this.progress += 50
  345. } catch (err) {
  346. this.$store.commit('pushGraphError', err)
  347. this.isLoading = false
  348. return
  349. }
  350. }
  351. // -> Import Content
  352. if (this.wantContent) {
  353. try {
  354. const resp = await this.$apollo.query({
  355. query: storageTargetsQuery,
  356. fetchPolicy: 'network-only'
  357. })
  358. if (_.has(resp, 'data.storage.targets')) {
  359. this.progress += 10
  360. let targets = resp.data.storage.targets.map(str => {
  361. let nStr = {
  362. ...str,
  363. config: _.sortBy(str.config.map(cfg => ({
  364. ...cfg,
  365. value: JSON.parse(cfg.value)
  366. })), [t => t.value.order])
  367. }
  368. // -> Setup Git Module
  369. if (this.contentMode === 'git' && nStr.key === 'git') {
  370. nStr.isEnabled = true
  371. nStr.mode = 'sync'
  372. nStr.syncInterval = 'PT5M'
  373. nStr.config = [
  374. { key: 'authType', value: { value: this.gitAuthMode } },
  375. { key: 'repoUrl', value: { value: this.gitRepoUrl } },
  376. { key: 'branch', value: { value: this.gitRepoBranch } },
  377. { key: 'sshPrivateKeyMode', value: { value: 'contents' } },
  378. { key: 'sshPrivateKeyPath', value: { value: '' } },
  379. { key: 'sshPrivateKeyContent', value: { value: this.gitPrivKey } },
  380. { key: 'verifySSL', value: { value: this.gitVerifySSL } },
  381. { key: 'basicUsername', value: { value: this.gitUsername } },
  382. { key: 'basicPassword', value: { value: this.gitPassword } },
  383. { key: 'defaultEmail', value: { value: this.gitUserEmail } },
  384. { key: 'defaultName', value: { value: this.gitUserName } },
  385. { key: 'localRepoPath', value: { value: this.gitRepoPath } },
  386. { key: 'gitBinaryPath', value: { value: '' } }
  387. ]
  388. }
  389. // -> Setup Disk Module
  390. if (this.contentMode === 'disk' && nStr.key === 'disk') {
  391. nStr.isEnabled = true
  392. nStr.mode = 'push'
  393. nStr.syncInterval = 'P0D'
  394. nStr.config = [
  395. { key: 'path', value: { value: this.contentPath } },
  396. { key: 'createDailyBackups', value: { value: false } }
  397. ]
  398. }
  399. return nStr
  400. })
  401. // -> Save storage modules configuration
  402. const respSv = await this.$apollo.mutate({
  403. mutation: targetsSaveMutation,
  404. variables: {
  405. targets: targets.map(tgt => _.pick(tgt, [
  406. 'isEnabled',
  407. 'key',
  408. 'config',
  409. 'mode',
  410. 'syncInterval'
  411. ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
  412. }
  413. })
  414. const respObj = _.get(respSv, 'data.storage.updateTargets', {})
  415. if (!_.get(respObj, 'responseResult.succeeded', false)) {
  416. throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occurred'))
  417. }
  418. this.progress += 10
  419. // -> Wait for success sync
  420. let statusAttempts = 0
  421. while (statusAttempts < 10) {
  422. statusAttempts++
  423. const respStatus = await this.$apollo.query({
  424. query: storageStatusQuery,
  425. fetchPolicy: 'network-only'
  426. })
  427. if (_.has(respStatus, 'data.storage.status[0]')) {
  428. const st = _.find(respStatus.data.storage.status, ['key', this.contentMode])
  429. if (!st) {
  430. throw new Error('Storage target could not be configured.')
  431. }
  432. switch (st.status) {
  433. case 'pending':
  434. if (statusAttempts >= 10) {
  435. throw new Error('Storage target is stuck in pending state. Try again.')
  436. } else {
  437. continue
  438. }
  439. case 'operational':
  440. statusAttempts = 10
  441. break
  442. case 'error':
  443. throw new Error(st.message)
  444. }
  445. } else {
  446. throw new Error('Failed to fetch storage sync status.')
  447. }
  448. }
  449. this.progress += 15
  450. // -> Perform import all
  451. const respImport = await this.$apollo.mutate({
  452. mutation: targetExecuteActionMutation,
  453. variables: {
  454. targetKey: this.contentMode,
  455. handler: 'importAll'
  456. }
  457. })
  458. const respImportObj = _.get(respImport, 'data.storage.executeAction', {})
  459. if (!_.get(respImportObj, 'responseResult.succeeded', false)) {
  460. throw new Error(_.get(respImportObj, 'responseResult.message', 'An unexpected error occurred'))
  461. }
  462. this.progress += 15
  463. } else {
  464. throw new Error('Failed to fetch storage targets.')
  465. }
  466. } catch (err) {
  467. this.$store.commit('pushGraphError', err)
  468. this.isLoading = false
  469. return
  470. }
  471. }
  472. this.isLoading = false
  473. this.isSuccess = true
  474. }, 1500)
  475. }
  476. }
  477. }
  478. </script>
  479. <style lang='scss'>
  480. </style>