SystemRestoreContent.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. <script setup lang="ts">
  2. import type { RestoreOptions, RestoreResponse } from '@/api/backup'
  3. import type { UploadFile } from 'ant-design-vue'
  4. import backup from '@/api/backup'
  5. import { InboxOutlined } from '@ant-design/icons-vue'
  6. import { message } from 'ant-design-vue'
  7. // Define props using TypeScript interface
  8. interface SystemRestoreProps {
  9. showTitle?: boolean
  10. showNginxOptions?: boolean
  11. }
  12. // Define emits using TypeScript interface
  13. interface SystemRestoreEmits {
  14. (e: 'restoreSuccess', options: { restoreNginx: boolean, restoreNginxUI: boolean }): void
  15. (e: 'restoreError', error: Error): void
  16. }
  17. withDefaults(defineProps<SystemRestoreProps>(), {
  18. showTitle: true,
  19. showNginxOptions: true,
  20. })
  21. const emit = defineEmits<SystemRestoreEmits>()
  22. // Use UploadFile from ant-design-vue
  23. const uploadFiles = ref<UploadFile[]>([])
  24. const isRestoring = ref(false)
  25. const formModel = reactive({
  26. securityToken: '',
  27. restoreNginx: true,
  28. restoreNginxUI: true,
  29. verifyHash: true,
  30. })
  31. // Add two variables to control modal display and countdown
  32. const showRestoreModal = ref(false)
  33. const countdown = ref(5)
  34. const countdownTimer = ref<ReturnType<typeof setInterval> | null>(null)
  35. // Reset countdown function
  36. function resetCountdown() {
  37. countdown.value = 5
  38. showRestoreModal.value = true
  39. // Clear any existing timer
  40. if (countdownTimer.value) {
  41. clearInterval(countdownTimer.value)
  42. }
  43. // Start countdown timer
  44. countdownTimer.value = setInterval(() => {
  45. countdown.value--
  46. if (countdown.value <= 0 && countdownTimer.value) {
  47. clearInterval(countdownTimer.value)
  48. }
  49. }, 1000)
  50. }
  51. // Handle OK button click
  52. function handleModalOk() {
  53. if (countdownTimer.value) {
  54. clearInterval(countdownTimer.value)
  55. }
  56. // Emit success event with restore options
  57. emit('restoreSuccess', {
  58. restoreNginx: formModel.restoreNginx,
  59. restoreNginxUI: formModel.restoreNginxUI,
  60. })
  61. }
  62. function handleBeforeUpload(file: File) {
  63. // Check if file type is zip
  64. const isZip = file.name.toLowerCase().endsWith('.zip')
  65. if (!isZip) {
  66. message.error($gettext('Only zip files are allowed'))
  67. uploadFiles.value = []
  68. return
  69. }
  70. // Create UploadFile object and directly manage uploadFiles
  71. const uploadFile = {
  72. uid: Date.now().toString(),
  73. name: file.name,
  74. status: 'done',
  75. size: file.size,
  76. type: file.type,
  77. originFileObj: file,
  78. } as UploadFile
  79. // Keep only the current file
  80. uploadFiles.value = [uploadFile]
  81. // Prevent default upload behavior
  82. return false
  83. }
  84. // Handle file removal
  85. function handleRemove() {
  86. uploadFiles.value = []
  87. }
  88. async function doRestore() {
  89. if (uploadFiles.value.length === 0) {
  90. message.warning($gettext('Please select a backup file'))
  91. return
  92. }
  93. if (!formModel.securityToken) {
  94. message.warning($gettext('Please enter the security token'))
  95. return
  96. }
  97. try {
  98. isRestoring.value = true
  99. const uploadedFile = uploadFiles.value[0]
  100. if (!uploadedFile.originFileObj) {
  101. message.error($gettext('Invalid file object'))
  102. return
  103. }
  104. const options: RestoreOptions = {
  105. backup_file: uploadedFile.originFileObj,
  106. security_token: formModel.securityToken,
  107. restore_nginx: formModel.restoreNginx,
  108. restore_nginx_ui: formModel.restoreNginxUI,
  109. verify_hash: formModel.verifyHash,
  110. }
  111. const data = await backup.restoreBackup(options) as RestoreResponse
  112. message.success($gettext('Restore completed successfully'))
  113. if (data.nginx_restored) {
  114. message.info($gettext('Nginx configuration has been restored'))
  115. }
  116. if (data.nginx_ui_restored) {
  117. message.info($gettext('Nginx UI configuration has been restored'))
  118. // If UI was restored, show the countdown modal
  119. resetCountdown()
  120. }
  121. else {
  122. // If UI was not restored, emit success event directly
  123. emit('restoreSuccess', {
  124. restoreNginx: formModel.restoreNginx,
  125. restoreNginxUI: formModel.restoreNginxUI,
  126. })
  127. }
  128. if (data.hash_match === false && formModel.verifyHash) {
  129. message.warning($gettext('Backup file integrity check failed, it may have been tampered with'))
  130. }
  131. // Reset form after successful restore
  132. uploadFiles.value = []
  133. formModel.securityToken = ''
  134. }
  135. catch (error) {
  136. console.error('Restore failed:', error)
  137. emit('restoreError', error instanceof Error ? error : new Error(String(error)))
  138. }
  139. finally {
  140. isRestoring.value = false
  141. }
  142. }
  143. </script>
  144. <template>
  145. <div>
  146. <ACard v-if="showTitle" :title="$gettext('System Restore')" :bordered="false">
  147. <AAlert
  148. show-icon
  149. type="warning"
  150. :message="$gettext('Warning: Restore operation will overwrite current configurations. Make sure you have a valid backup file and security token, and carefully select what to restore.')"
  151. class="mb-4"
  152. />
  153. <AUploadDragger
  154. :file-list="uploadFiles"
  155. :multiple="false"
  156. :max-count="1"
  157. accept=".zip"
  158. :before-upload="handleBeforeUpload"
  159. @remove="handleRemove"
  160. >
  161. <p class="ant-upload-drag-icon">
  162. <InboxOutlined />
  163. </p>
  164. <p class="ant-upload-text">
  165. {{ $gettext('Click or drag backup file to this area to upload') }}
  166. </p>
  167. <p class="ant-upload-hint">
  168. {{ $gettext('Supported file type: .zip') }}
  169. </p>
  170. </AUploadDragger>
  171. <AForm
  172. v-if="uploadFiles.length > 0"
  173. :model="formModel"
  174. layout="vertical"
  175. class="mt-4"
  176. >
  177. <AFormItem :label="$gettext('Security Token')">
  178. <AInput
  179. v-model:value="formModel.securityToken"
  180. :placeholder="$gettext('Please enter the security token received during backup')"
  181. />
  182. </AFormItem>
  183. <AFormItem>
  184. <ACheckbox v-model:checked="formModel.verifyHash" :disabled="true">
  185. {{ $gettext('Verify Backup File Integrity') }}
  186. </ACheckbox>
  187. </AFormItem>
  188. <template v-if="showNginxOptions">
  189. <AFormItem>
  190. <ACheckbox v-model:checked="formModel.restoreNginx">
  191. {{ $gettext('Restore Nginx Configuration') }}
  192. </ACheckbox>
  193. <div class="text-gray-500 ml-6 mt-1 text-sm">
  194. <p class="mb-0">
  195. {{ $gettext('This will restore all Nginx configuration files. Nginx will restart after the restoration is complete.') }}
  196. </p>
  197. </div>
  198. </AFormItem>
  199. <AFormItem>
  200. <ACheckbox v-model:checked="formModel.restoreNginxUI">
  201. {{ $gettext('Restore Nginx UI Configuration') }}
  202. </ACheckbox>
  203. <div class="text-gray-500 ml-6 mt-1 text-sm">
  204. <p class="mb-0">
  205. {{ $gettext('This will restore configuration files and database. Nginx UI will restart after the restoration is complete.') }}
  206. </p>
  207. </div>
  208. </AFormItem>
  209. </template>
  210. <AFormItem>
  211. <AButton type="primary" :loading="isRestoring" @click="doRestore">
  212. {{ $gettext('Start Restore') }}
  213. </AButton>
  214. </AFormItem>
  215. </AForm>
  216. </ACard>
  217. <div v-else>
  218. <AAlert
  219. show-icon
  220. type="warning"
  221. :message="$gettext('Warning: Restore operation will overwrite current configurations. Make sure you have a valid backup file and security token, and carefully select what to restore.')"
  222. class="mb-4"
  223. />
  224. <AUploadDragger
  225. :file-list="uploadFiles"
  226. :multiple="false"
  227. :max-count="1"
  228. accept=".zip"
  229. :before-upload="handleBeforeUpload"
  230. @remove="handleRemove"
  231. >
  232. <p class="ant-upload-drag-icon">
  233. <InboxOutlined />
  234. </p>
  235. <p class="ant-upload-text">
  236. {{ $gettext('Click or drag backup file to this area to upload') }}
  237. </p>
  238. <p class="ant-upload-hint">
  239. {{ $gettext('Supported file type: .zip') }}
  240. </p>
  241. </AUploadDragger>
  242. <AForm
  243. v-if="uploadFiles.length > 0"
  244. :model="formModel"
  245. layout="vertical"
  246. class="mt-4"
  247. >
  248. <AFormItem :label="$gettext('Security Token')">
  249. <AInput
  250. v-model:value="formModel.securityToken"
  251. :placeholder="$gettext('Please enter the security token received during backup')"
  252. />
  253. </AFormItem>
  254. <AFormItem>
  255. <ACheckbox v-model:checked="formModel.verifyHash" :disabled="true">
  256. {{ $gettext('Verify Backup File Integrity') }}
  257. </ACheckbox>
  258. </AFormItem>
  259. <template v-if="showNginxOptions">
  260. <AFormItem>
  261. <ACheckbox v-model:checked="formModel.restoreNginx">
  262. {{ $gettext('Restore Nginx Configuration') }}
  263. </ACheckbox>
  264. <div class="text-gray-500 ml-6 mt-1 text-sm">
  265. <p class="mb-0">
  266. {{ $gettext('This will restore all Nginx configuration files. Nginx will restart after the restoration is complete.') }}
  267. </p>
  268. </div>
  269. </AFormItem>
  270. <AFormItem>
  271. <ACheckbox v-model:checked="formModel.restoreNginxUI">
  272. {{ $gettext('Restore Nginx UI Configuration') }}
  273. </ACheckbox>
  274. <div class="text-gray-500 ml-6 mt-1 text-sm">
  275. <p class="mb-0">
  276. {{ $gettext('This will restore configuration files and database. Nginx UI will restart after the restoration is complete.') }}
  277. </p>
  278. </div>
  279. </AFormItem>
  280. </template>
  281. <AFormItem>
  282. <AButton type="primary" :loading="isRestoring" @click="doRestore">
  283. {{ $gettext('Start Restore') }}
  284. </AButton>
  285. </AFormItem>
  286. </AForm>
  287. </div>
  288. <!-- Modal for countdown -->
  289. <AModal
  290. v-model:open="showRestoreModal"
  291. :title="$gettext('Automatic Restart')"
  292. :mask-closable="false"
  293. >
  294. <p>
  295. {{ $gettext('Nginx UI configuration has been restored and will restart automatically in a few seconds.') }}
  296. </p>
  297. <p v-if="countdown > 0">
  298. {{ $gettext('You can close this dialog in %{countdown} seconds', { countdown: countdown.toString() }) }}
  299. </p>
  300. <p v-else>
  301. {{ $gettext('You can close this dialog now') }}
  302. </p>
  303. <template #footer>
  304. <AButton
  305. type="primary"
  306. :disabled="countdown > 0"
  307. @click="handleModalOk"
  308. >
  309. {{ countdown > 0 ? `OK (${countdown}s)` : 'OK' }}
  310. </AButton>
  311. </template>
  312. </AModal>
  313. </div>
  314. </template>