mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 02:15:48 +02:00
feat: update restore process with countdown modal and improved symlink handling
This commit is contained in:
parent
000e28942a
commit
4c2487580e
6 changed files with 168 additions and 59 deletions
|
@ -120,7 +120,7 @@ func RestoreBackup(c *gin.Context) {
|
||||||
|
|
||||||
if restoreNginxUI {
|
if restoreNginxUI {
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
// gracefully restart
|
// gracefully restart
|
||||||
overseer.Restart()
|
overseer.Restart()
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -3,26 +3,25 @@ import type { RestoreOptions, RestoreResponse } from '@/api/backup'
|
||||||
import type { UploadFile } from 'ant-design-vue'
|
import type { UploadFile } from 'ant-design-vue'
|
||||||
import backup from '@/api/backup'
|
import backup from '@/api/backup'
|
||||||
import { InboxOutlined } from '@ant-design/icons-vue'
|
import { InboxOutlined } from '@ant-design/icons-vue'
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
// Define props using TypeScript interface
|
// Define props using TypeScript interface
|
||||||
interface SystemRestoreProps {
|
interface SystemRestoreProps {
|
||||||
showTitle?: boolean
|
showTitle?: boolean
|
||||||
showNginxOptions?: boolean
|
showNginxOptions?: boolean
|
||||||
onRestoreSuccess?: (data: RestoreResponse) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define emits using TypeScript interface
|
// Define emits using TypeScript interface
|
||||||
interface SystemRestoreEmits {
|
interface SystemRestoreEmits {
|
||||||
(e: 'restoreSuccess', data: RestoreResponse): void
|
(e: 'restoreSuccess', options: { restoreNginx: boolean, restoreNginxUI: boolean }): void
|
||||||
(e: 'restoreError', error: Error): void
|
(e: 'restoreError', error: Error): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<SystemRestoreProps>(), {
|
withDefaults(defineProps<SystemRestoreProps>(), {
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
showNginxOptions: true,
|
showNginxOptions: true,
|
||||||
onRestoreSuccess: () => null,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<SystemRestoreEmits>()
|
const emit = defineEmits<SystemRestoreEmits>()
|
||||||
|
|
||||||
// Use UploadFile from ant-design-vue
|
// Use UploadFile from ant-design-vue
|
||||||
|
@ -36,6 +35,42 @@ const formModel = reactive({
|
||||||
verifyHash: true,
|
verifyHash: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 添加两个变量控制模态框显示和倒计时
|
||||||
|
const showRestoreModal = ref(false)
|
||||||
|
const countdown = ref(5)
|
||||||
|
const countdownTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
|
||||||
|
// Reset countdown function
|
||||||
|
function resetCountdown() {
|
||||||
|
countdown.value = 5
|
||||||
|
showRestoreModal.value = true
|
||||||
|
|
||||||
|
// Clear any existing timer
|
||||||
|
if (countdownTimer.value) {
|
||||||
|
clearInterval(countdownTimer.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start countdown timer
|
||||||
|
countdownTimer.value = setInterval(() => {
|
||||||
|
countdown.value--
|
||||||
|
if (countdown.value <= 0 && countdownTimer.value) {
|
||||||
|
clearInterval(countdownTimer.value)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle OK button click
|
||||||
|
function handleModalOk() {
|
||||||
|
if (countdownTimer.value) {
|
||||||
|
clearInterval(countdownTimer.value)
|
||||||
|
}
|
||||||
|
// Emit success event with restore options
|
||||||
|
emit('restoreSuccess', {
|
||||||
|
restoreNginx: formModel.restoreNginx,
|
||||||
|
restoreNginxUI: formModel.restoreNginxUI,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function handleBeforeUpload(file: File) {
|
function handleBeforeUpload(file: File) {
|
||||||
// Check if file type is zip
|
// Check if file type is zip
|
||||||
const isZip = file.name.toLowerCase().endsWith('.zip')
|
const isZip = file.name.toLowerCase().endsWith('.zip')
|
||||||
|
@ -105,13 +140,14 @@ async function doRestore() {
|
||||||
|
|
||||||
if (data.nginx_ui_restored) {
|
if (data.nginx_ui_restored) {
|
||||||
message.info($gettext('Nginx UI configuration has been restored'))
|
message.info($gettext('Nginx UI configuration has been restored'))
|
||||||
|
// If UI was restored, show the countdown modal
|
||||||
// Show warning modal about restart
|
resetCountdown()
|
||||||
Modal.warning({
|
}
|
||||||
title: $gettext('Automatic Restart'),
|
else {
|
||||||
content: $gettext('Nginx UI configuration has been restored and will restart automatically in a few seconds.'),
|
// If UI was not restored, emit success event directly
|
||||||
okText: $gettext('OK'),
|
emit('restoreSuccess', {
|
||||||
maskClosable: false,
|
restoreNginx: formModel.restoreNginx,
|
||||||
|
restoreNginxUI: formModel.restoreNginxUI,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,12 +158,6 @@ async function doRestore() {
|
||||||
// Reset form after successful restore
|
// Reset form after successful restore
|
||||||
uploadFiles.value = []
|
uploadFiles.value = []
|
||||||
formModel.securityToken = ''
|
formModel.securityToken = ''
|
||||||
// Emit success event
|
|
||||||
emit('restoreSuccess', data)
|
|
||||||
// Call the callback function if provided
|
|
||||||
if (props.onRestoreSuccess) {
|
|
||||||
props.onRestoreSuccess(data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('Restore failed:', error)
|
console.error('Restore failed:', error)
|
||||||
|
@ -295,5 +325,31 @@ async function doRestore() {
|
||||||
</AFormItem>
|
</AFormItem>
|
||||||
</AForm>
|
</AForm>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal for countdown -->
|
||||||
|
<AModal
|
||||||
|
v-model:open="showRestoreModal"
|
||||||
|
:title="$gettext('Automatic Restart')"
|
||||||
|
:mask-closable="false"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{{ $gettext('Nginx UI configuration has been restored and will restart automatically in a few seconds.') }}
|
||||||
|
</p>
|
||||||
|
<p v-if="countdown > 0">
|
||||||
|
{{ $gettext('You can close this dialog in') }} {{ countdown }} {{ $gettext('seconds') }}
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
|
{{ $gettext('You can close this dialog now') }}
|
||||||
|
</p>
|
||||||
|
<template #footer>
|
||||||
|
<AButton
|
||||||
|
type="primary"
|
||||||
|
:disabled="countdown > 0"
|
||||||
|
@click="handleModalOk"
|
||||||
|
>
|
||||||
|
{{ countdown > 0 ? `OK (${countdown}s)` : 'OK' }}
|
||||||
|
</AButton>
|
||||||
|
</template>
|
||||||
|
</AModal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{"version":"2.0.0-rc.4","build_id":2,"total_build":387}
|
{"version":"2.0.0-rc.4","build_id":6,"total_build":391}
|
|
@ -102,9 +102,14 @@ function onSubmit() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRestoreSuccess(): void {
|
function handleRestoreSuccess(options: { restoreNginx: boolean, restoreNginxUI: boolean }): void {
|
||||||
message.success($gettext('System restored successfully. Please log in.'))
|
message.success($gettext('System restored successfully.'))
|
||||||
router.push('/login')
|
|
||||||
|
// Only redirect to login page if Nginx UI was restored
|
||||||
|
if (options.restoreNginxUI) {
|
||||||
|
message.info($gettext('Please log in.'))
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -185,7 +190,7 @@ function handleRestoreSuccess(): void {
|
||||||
<TabPane key="2" :tab="$gettext('Restore from Backup')">
|
<TabPane key="2" :tab="$gettext('Restore from Backup')">
|
||||||
<SystemRestoreContent
|
<SystemRestoreContent
|
||||||
:show-title="false"
|
:show-title="false"
|
||||||
:on-restore-success="handleRestoreSuccess"
|
@restore-success="handleRestoreSuccess"
|
||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SystemRestoreContent from '@/components/SystemRestore/SystemRestoreContent.vue'
|
import SystemRestoreContent from '@/components/SystemRestore/SystemRestoreContent.vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
function handleRestoreSuccess(options: { restoreNginx: boolean, restoreNginxUI: boolean }): void {
|
||||||
|
message.success($gettext('System restored successfully.'))
|
||||||
|
|
||||||
|
// Only redirect to login page if Nginx UI was restored
|
||||||
|
if (options.restoreNginxUI) {
|
||||||
|
message.info($gettext('Please log in.'))
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SystemRestoreContent :show-title="true" />
|
<SystemRestoreContent :show-title="true" @restore-success="handleRestoreSuccess" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -207,44 +207,61 @@ func extractZipFile(file *zip.File, destDir string) error {
|
||||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("invalid symlink target: %s", linkTarget))
|
return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("invalid symlink target: %s", linkTarget))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get nginx modules path
|
// Get allowed paths for symlinks
|
||||||
|
confPath := nginx.GetConfPath()
|
||||||
modulesPath := nginx.GetModulesPath()
|
modulesPath := nginx.GetModulesPath()
|
||||||
|
|
||||||
// Handle system directory symlinks
|
// Check if symlink target is to an allowed path (conf path or modules path)
|
||||||
if strings.HasPrefix(cleanLinkTarget, modulesPath) {
|
isAllowedSymlink := false
|
||||||
// For nginx modules, we'll create a relative symlink to the modules directory
|
|
||||||
relPath, err := filepath.Rel(filepath.Dir(filePath), modulesPath)
|
// Check if link points to modules path
|
||||||
if err != nil {
|
if filepath.IsAbs(cleanLinkTarget) && (cleanLinkTarget == modulesPath || strings.HasPrefix(cleanLinkTarget, modulesPath+string(filepath.Separator))) {
|
||||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("failed to convert modules path to relative: %v", err))
|
isAllowedSymlink = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if link points to nginx conf path
|
||||||
|
if filepath.IsAbs(cleanLinkTarget) && (cleanLinkTarget == confPath || strings.HasPrefix(cleanLinkTarget, confPath+string(filepath.Separator))) {
|
||||||
|
isAllowedSymlink = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle absolute paths
|
||||||
|
if filepath.IsAbs(cleanLinkTarget) {
|
||||||
|
// Remove any existing file/link at the target path
|
||||||
|
if err := os.RemoveAll(filePath); err != nil && !os.IsNotExist(err) {
|
||||||
|
// Ignoring error, continue creating symlink
|
||||||
}
|
}
|
||||||
cleanLinkTarget = relPath
|
|
||||||
} else if filepath.IsAbs(cleanLinkTarget) {
|
// If this is a symlink to an allowed path, create it
|
||||||
// For other absolute paths, we'll create a directory instead of a symlink
|
if isAllowedSymlink {
|
||||||
|
if err := os.Symlink(cleanLinkTarget, filePath); err != nil {
|
||||||
|
return cosy.WrapErrorWithParams(ErrCreateSymlink, fmt.Sprintf("failed to create symlink %s -> %s: %v", filePath, cleanLinkTarget, err))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, fallback to creating a directory
|
||||||
if err := os.MkdirAll(filePath, 0755); err != nil {
|
if err := os.MkdirAll(filePath, 0755); err != nil {
|
||||||
return cosy.WrapErrorWithParams(ErrCreateDir, fmt.Sprintf("failed to create directory %s: %v", filePath, err))
|
return cosy.WrapErrorWithParams(ErrCreateDir, fmt.Sprintf("failed to create directory %s: %v", filePath, err))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the link target doesn't escape the destination directory
|
// For relative symlinks, verify they don't escape the destination directory
|
||||||
absLinkTarget := filepath.Clean(filepath.Join(filepath.Dir(filePath), cleanLinkTarget))
|
absLinkTarget := filepath.Clean(filepath.Join(filepath.Dir(filePath), cleanLinkTarget))
|
||||||
if !strings.HasPrefix(absLinkTarget, destDirAbs+string(os.PathSeparator)) {
|
if !strings.HasPrefix(absLinkTarget, destDirAbs+string(os.PathSeparator)) {
|
||||||
// For nginx modules, we'll create a directory instead of a symlink
|
// Create directory instead of symlink if the target is outside destination
|
||||||
if strings.HasPrefix(linkTarget, modulesPath) {
|
if err := os.MkdirAll(filePath, 0755); err != nil {
|
||||||
if err := os.MkdirAll(filePath, 0755); err != nil {
|
return cosy.WrapErrorWithParams(ErrCreateDir, fmt.Sprintf("failed to create directory %s: %v", filePath, err))
|
||||||
return cosy.WrapErrorWithParams(ErrCreateDir, fmt.Sprintf("failed to create modules directory %s: %v", filePath, err))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("symlink target %s is outside destination directory %s", absLinkTarget, destDirAbs))
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any existing file/link at the target path
|
// Remove any existing file/link at the target path
|
||||||
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
|
if err := os.RemoveAll(filePath); err != nil && !os.IsNotExist(err) {
|
||||||
// Ignoring error, continue creating symlink
|
// Ignoring error, continue creating symlink
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the symlink
|
// Create the symlink for relative paths within destination
|
||||||
if err := os.Symlink(cleanLinkTarget, filePath); err != nil {
|
if err := os.Symlink(cleanLinkTarget, filePath); err != nil {
|
||||||
return cosy.WrapErrorWithParams(ErrCreateSymlink, fmt.Sprintf("failed to create symlink %s -> %s: %v", filePath, cleanLinkTarget, err))
|
return cosy.WrapErrorWithParams(ErrCreateSymlink, fmt.Sprintf("failed to create symlink %s -> %s: %v", filePath, cleanLinkTarget, err))
|
||||||
}
|
}
|
||||||
|
@ -361,20 +378,9 @@ func restoreNginxConfigs(nginxBackupDir string) error {
|
||||||
return ErrNginxConfigDirEmpty
|
return ErrNginxConfigDirEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove all contents in the destination directory first
|
// Recursively clean destination directory preserving the directory structure
|
||||||
// Read directory entries
|
if err := cleanDirectoryPreservingStructure(destDir); err != nil {
|
||||||
entries, err := os.ReadDir(destDir)
|
return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, "failed to clean directory: "+err.Error())
|
||||||
if err != nil {
|
|
||||||
return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, "failed to read directory: "+err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove each entry
|
|
||||||
for _, entry := range entries {
|
|
||||||
entryPath := filepath.Join(destDir, entry.Name())
|
|
||||||
err := os.RemoveAll(entryPath)
|
|
||||||
if err != nil {
|
|
||||||
return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, "failed to remove: "+err.Error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy files from backup to nginx config directory
|
// Copy files from backup to nginx config directory
|
||||||
|
@ -385,6 +391,35 @@ func restoreNginxConfigs(nginxBackupDir string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanDirectoryPreservingStructure removes all files and symlinks in a directory
|
||||||
|
// but preserves the directory structure itself
|
||||||
|
func cleanDirectoryPreservingStructure(dir string) error {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
path := filepath.Join(dir, entry.Name())
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve symlinks - they will be handled separately during restore
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.RemoveAll(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// restoreNginxUIConfig restores nginx-ui configuration files
|
// restoreNginxUIConfig restores nginx-ui configuration files
|
||||||
func restoreNginxUIConfig(nginxUIBackupDir string) error {
|
func restoreNginxUIConfig(nginxUIBackupDir string) error {
|
||||||
// Get config directory
|
// Get config directory
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue