| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507 | <template lang='pug'>  v-card    v-toolbar(flat, color='primary', dark, dense)      .subtitle-1 {{ $t('admin:utilities.importv1Title') }}    v-card-text      .text-center        img.animated.fadeInUp.wait-p1s(src='/_assets/svg/icon-software.svg')        .body-2 Import from Wiki.js 1.x      v-divider.my-4      .body-2 Data from a Wiki.js 1.x installation can easily be imported using this tool. What do you want to import?      v-checkbox(        label='Content + Uploads'        value='content'        color='deep-orange darken-2'        v-model='importFilters'        hide-details        )        template(v-slot:label)          strong.deep-orange--text.text--darken-2 Content + Uploads      .pl-8(v-if='wantContent')        v-radio-group(v-model='contentMode', hide-details)          v-radio(            value='git'            color='primary'            )            template(v-slot:label)              div                span Import from Git Connection                .caption: em #[strong.primary--text Recommended] | The Git storage module will also be configured for you.        .pl-8.mt-5(v-if='needGit')          v-row            v-col(cols='8')              v-select(                label='Authentication Mode'                :items='gitAuthModes'                v-model='gitAuthMode'                outlined                hide-details              )            v-col(cols='4')              v-switch(                label='Verify SSL Certificate'                v-model='gitVerifySSL'                hide-details                color='primary'              )            v-col(cols='8')              v-text-field(                outlined                label='Repository URL'                :placeholder='(gitAuthMode === `ssh`) ? `e.g. git@github.com:orgname/repo.git` : `e.g. https://github.com/orgname/repo.git`'                hide-details                v-model='gitRepoUrl'              )            v-col(cols='4')              v-text-field(                label='Branch'                placeholder='e.g. master'                v-model='gitRepoBranch'                outlined                hide-details              )            v-col(v-if='gitAuthMode === `ssh`', cols='12')              v-textarea(                outlined                label='Private Key Contents'                placeholder='-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----'                hide-details                v-model='gitPrivKey'              )            template(v-else-if='gitAuthMode === `basic`')              v-col(cols='6')                v-text-field(                  label='Username'                  v-model='gitUsername'                  outlined                  hide-details                )              v-col(cols='6')                v-text-field(                  type='password'                  label='Password / PAT'                  v-model='gitPassword'                  outlined                  hide-details                )            v-col(cols='6')              v-text-field(                label='Default Author Email'                placeholder='e.g. name@company.com'                v-model='gitUserEmail'                outlined                hide-details              )            v-col(cols='6')              v-text-field(                label='Default Author Name'                placeholder='e.g. John Smith'                v-model='gitUserName'                outlined                hide-details              )            v-col(cols='12')              v-text-field(                label='Local Repository Path'                placeholder='e.g. ./data/repo'                v-model='gitRepoPath'                outlined                hide-details              )              .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.          v-alert(color='deep-orange', outlined, icon='mdi-alert', prominent)            .body-2 - Note that if you already configured the git storage module, its configuration will be replaced with the above.            .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.        v-radio-group(v-model='contentMode', hide-details)          v-divider          v-radio.mt-3(            value='disk'            color='primary'            )            template(v-slot:label)              div                span Import from local folder                .caption: em Choose this option only if you didn't have git configured in your Wiki.js 1.x installation.        .pl-8.mt-5(v-if='needDisk')          v-text-field(            outlined            label='Content Repo Path'            hint='The absolute path to where the Wiki.js 1.x content is stored on disk.'            persistent-hint            v-model='contentPath'          )      v-checkbox(        label='Users'        value='users'        color='deep-orange darken-2'        v-model='importFilters'        hide-details        )        template(v-slot:label)          strong.deep-orange--text.text--darken-2 Users      .pl-8.mt-5(v-if='wantUsers')        v-text-field(          outlined          label='MongoDB Connection String'          hint='The connection string to connect to the Wiki.js 1.x MongoDB database.'          persistent-hint          v-model='dbConnStr'        )        v-radio-group(v-model='groupMode', hide-details, mandatory)          v-radio(            value='MULTI'            color='primary'            )            template(v-slot:label)              div                span Create groups for each unique user permissions configuration                .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.          v-divider          v-radio.mt-3(            value='SINGLE'            color='primary'            )            template(v-slot:label)              div                span Create a single group with all imported users                .caption: em The new group will have read permissions enabled by default.          v-divider          v-radio.mt-3(            value='NONE'            color='primary'            )            template(v-slot:label)              div                span Don't create any group                .caption: em Users will not be able to access your wiki until they are assigned to a group.        v-alert.mt-5(color='deep-orange', outlined, icon='mdi-alert', prominent)          .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.          .caption.grey--text You must first delete from this installation any user you want to migrate over from the old installation.    v-card-chin      v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='!wantUsers && !wantContent', @click='startImport').ml-0        v-icon(left, color='white') mdi-database-import        span.white--text Start Import    v-dialog(      v-model='isLoading'      persistent      max-width='350'      )      v-card(color='deep-orange darken-2', dark)        v-card-text.pa-10.text-center          semipolar-spinner.animated.fadeIn(            :animation-duration='1500'            :size='65'            color='#FFF'            style='margin: 0 auto;'          )          .mt-5.body-1.white--text Importing from Wiki.js 1.x...          .caption Please wait          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 Import completed          template(v-if='wantUsers')            .body-2              span #[strong {{successUsers}}] users imported              v-btn.text-none.ml-3(                v-if='failedUsers.length > 0'                text                color='white'                dark                @click='showFailedUsers = true'                )                v-icon(left) mdi-alert                span {{failedUsers.length}} failed            .body-2 #[strong {{successGroups}}] groups created        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='showFailedUsers'      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 Failed User Imports          v-spacer          v-btn.px-5(            color='white'            text            @click='showFailedUsers = false'            ) Close        v-simple-table(dense, fixed-header, height='300px')          template(v-slot:default)            thead              tr                th Provider                th Email                th Error            tbody              tr(v-for='(fusr, idx) in failedUsers', :key='`fusr-` + idx')                td {{fusr.provider}}                td {{fusr.email}}                td {{fusr.error}}</template><script>import _ from 'lodash'import { SemipolarSpinner } from 'epic-spinners'import utilityImportv1UsersMutation from 'gql/admin/utilities/utilities-mutation-importv1-users.gql'import storageTargetsQuery from 'gql/admin/storage/storage-query-targets.gql'import storageStatusQuery from 'gql/admin/storage/storage-query-status.gql'import targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql'import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'export default {  components: {    SemipolarSpinner  },  data() {    return {      importFilters: ['content', 'users'],      groupMode: 'MULTI',      contentMode: 'git',      dbConnStr: 'mongodb://',      contentPath: '/wiki-v1/repo',      isLoading: false,      isSuccess: false,      gitAuthMode: 'ssh',      gitAuthModes: [        { text: 'SSH', value: 'ssh' },        { text: 'Basic', value: 'basic' }      ],      gitVerifySSL: true,      gitRepoUrl: '',      gitRepoBranch: 'master',      gitPrivKey: '',      gitUsername: '',      gitPassword: '',      gitUserEmail: '',      gitUserName: '',      gitRepoPath: './data/repo',      progress: 0,      successGroups: 0,      successUsers: 0,      successPages: 0,      showFailedUsers: false,      failedUsers: []    }  },  computed: {    wantContent () {      return this.importFilters.indexOf('content') >= 0    },    wantUsers () {      return this.importFilters.indexOf('users') >= 0    },    needDisk () {      return this.contentMode === `disk`    },    needGit () {      return this.contentMode === `git`    }  },  methods: {    async startImport () {      this.isLoading = true      this.progress = 0      this.failedUsers = []      _.delay(async () => {        // -> Import Users        if (this.wantUsers) {          try {            const resp = await this.$apollo.mutate({              mutation: utilityImportv1UsersMutation,              variables: {                mongoDbConnString: this.dbConnStr,                groupMode: this.groupMode              }            })            const respObj = _.get(resp, 'data.system.importUsersFromV1', {})            if (!_.get(respObj, 'responseResult.succeeded', false)) {              throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occured'))            }            this.successUsers = _.get(respObj, 'usersCount', 0)            this.successGroups = _.get(respObj, 'groupsCount', 0)            this.failedUsers = _.get(respObj, 'failed', [])            this.progress += 50          } catch (err) {            this.$store.commit('pushGraphError', err)            this.isLoading = false            return          }        }        // -> Import Content        if (this.wantContent) {          try {            const resp = await this.$apollo.query({              query: storageTargetsQuery,              fetchPolicy: 'network-only'            })            if (_.has(resp, 'data.storage.targets')) {              this.progress += 10              let targets = resp.data.storage.targets.map(str => {                let nStr = {                  ...str,                  config: _.sortBy(str.config.map(cfg => ({                    ...cfg,                    value: JSON.parse(cfg.value)                  })), [t => t.value.order])                }                // -> Setup Git Module                if (this.contentMode === 'git' && nStr.key === 'git') {                  nStr.isEnabled = true                  nStr.mode = 'sync'                  nStr.syncInterval = 'PT5M'                  nStr.config = [                    { key: 'authType', value: { value: this.gitAuthMode } },                    { key: 'repoUrl', value: { value: this.gitRepoUrl } },                    { key: 'branch', value: { value: this.gitRepoBranch } },                    { key: 'sshPrivateKeyMode', value: { value: 'contents' } },                    { key: 'sshPrivateKeyPath', value: { value: '' } },                    { key: 'sshPrivateKeyContent', value: { value: this.gitPrivKey } },                    { key: 'verifySSL', value: { value: this.gitVerifySSL } },                    { key: 'basicUsername', value: { value: this.gitUsername } },                    { key: 'basicPassword', value: { value: this.gitPassword } },                    { key: 'defaultEmail', value: { value: this.gitUserEmail } },                    { key: 'defaultName', value: { value: this.gitUserName } },                    { key: 'localRepoPath', value: { value: this.gitRepoPath } },                    { key: 'gitBinaryPath', value: { value: '' } }                  ]                }                // -> Setup Disk Module                if (this.contentMode === 'disk' && nStr.key === 'disk') {                  nStr.isEnabled = true                  nStr.mode = 'push'                  nStr.syncInterval = 'P0D'                  nStr.config = [                    { key: 'path', value: { value: this.contentPath } },                    { key: 'createDailyBackups', value: { value: false } }                  ]                }                return nStr              })              // -> Save storage modules configuration              const respSv = await this.$apollo.mutate({                mutation: targetsSaveMutation,                variables: {                  targets: targets.map(tgt => _.pick(tgt, [                    'isEnabled',                    'key',                    'config',                    'mode',                    'syncInterval'                  ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))                }              })              const respObj = _.get(respSv, 'data.storage.updateTargets', {})              if (!_.get(respObj, 'responseResult.succeeded', false)) {                throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occured'))              }              this.progress += 10              // -> Wait for success sync              let statusAttempts = 0              while (statusAttempts < 10) {                statusAttempts++                const respStatus = await this.$apollo.query({                  query: storageStatusQuery,                  fetchPolicy: 'network-only'                })                if (_.has(respStatus, 'data.storage.status[0]')) {                  const st = _.find(respStatus.data.storage.status, ['key', this.contentMode])                  if (!st) {                    throw new Error('Storage target could not be configured.')                  }                  switch (st.status) {                    case 'pending':                      if (statusAttempts >= 10) {                        throw new Error('Storage target is stuck in pending state. Try again.')                      } else {                        continue                      }                    case 'operational':                      statusAttempts = 10                      break                    case 'error':                      throw new Error(st.message)                  }                } else {                  throw new Error('Failed to fetch storage sync status.')                }              }              this.progress += 15              // -> Perform import all              const respImport = await this.$apollo.mutate({                mutation: targetExecuteActionMutation,                variables: {                  targetKey: this.contentMode,                  handler: 'importAll'                }              })              const respImportObj = _.get(respImport, 'data.storage.executeAction', {})              if (!_.get(respImportObj, 'responseResult.succeeded', false)) {                throw new Error(_.get(respImportObj, 'responseResult.message', 'An unexpected error occured'))              }              this.progress += 15            } else {              throw new Error('Failed to fetch storage targets.')            }          } catch (err) {            this.$store.commit('pushGraphError', err)            this.isLoading = false            return          }        }        this.isLoading = false        this.isSuccess = true      }, 1500)    }  }}</script><style lang='scss'></style>
 |