From 789d451ea2be0efb299e848f7a3ca721fa64a230 Mon Sep 17 00:00:00 2001 From: ssongliu <73214554+ssongliu@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:59:28 +0800 Subject: [PATCH] feat: Add ZIP support for database backup uploads (#10246) Refs #7179 --- agent/app/service/backup_mysql.go | 94 ++++++++++++++---------- agent/app/service/backup_postgresql.go | 44 ++--------- frontend/src/components/upload/index.vue | 48 ++++++++---- frontend/src/lang/modules/en.ts | 7 +- frontend/src/lang/modules/ja.ts | 8 +- frontend/src/lang/modules/ko.ts | 7 +- frontend/src/lang/modules/ms.ts | 7 +- frontend/src/lang/modules/pt-br.ts | 8 +- frontend/src/lang/modules/ru.ts | 7 +- frontend/src/lang/modules/tr.ts | 7 +- frontend/src/lang/modules/zh-Hant.ts | 7 +- frontend/src/lang/modules/zh.ts | 7 +- 12 files changed, 125 insertions(+), 126 deletions(-) diff --git a/agent/app/service/backup_mysql.go b/agent/app/service/backup_mysql.go index 8c4190552..765b1f0cf 100644 --- a/agent/app/service/backup_mysql.go +++ b/agent/app/service/backup_mysql.go @@ -59,44 +59,12 @@ func (u *BackupService) MysqlRecover(req dto.CommonRecover) error { } func (u *BackupService) MysqlRecoverByUpload(req dto.CommonRecover) error { - file := req.File - fileName := path.Base(req.File) - if strings.HasSuffix(fileName, ".tar.gz") { - fileNameItem := time.Now().Format(constant.DateTimeSlimLayout) - dstDir := fmt.Sprintf("%s/%s", path.Dir(req.File), fileNameItem) - fileOp := files.NewFileOp() - if !fileOp.Stat(dstDir) { - if err := fileOp.CreateDir(dstDir, os.ModePerm); err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", dstDir, err) - } - } - if err := fileOp.TarGzExtractPro(req.File, dstDir, ""); err != nil { - _ = os.RemoveAll(dstDir) - return err - } - global.LOG.Infof("decompress file %s successful, now start to check test.sql is exist", req.File) - hasTestSql := false - _ = filepath.Walk(dstDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if !info.IsDir() && info.Name() == "test.sql" { - hasTestSql = true - file = path - fileName = "test.sql" - } - return nil - }) - if !hasTestSql { - _ = os.RemoveAll(dstDir) - return fmt.Errorf("no such file named test.sql in %s", fileName) - } - defer func() { - _ = os.RemoveAll(dstDir) - }() + recoveFile, err := loadSqlFile(req.File) + if err != nil { + return err } - - req.File = path.Dir(file) + "/" + fileName + req.File = recoveFile + defer os.RemoveAll(path.Dir(recoveFile)) if err := handleMysqlRecover(req, nil, false, req.TaskID); err != nil { return err } @@ -243,3 +211,55 @@ func doMysqlBackup(db DatabaseHelper, targetDir, fileName string) error { } return cli.Backup(backupInfo) } + +func loadSqlFile(file string) (string, error) { + if !strings.HasSuffix(file, ".tar.gz") && !strings.HasSuffix(file, ".zip") { + return file, nil + } + fileName := path.Base(file) + fileDir := path.Dir(file) + fileNameItem := time.Now().Format(constant.DateTimeSlimLayout) + dstDir := fmt.Sprintf("%s/%s", fileDir, fileNameItem) + _ = os.Mkdir(dstDir, constant.DirPerm) + if strings.HasSuffix(fileName, ".tar.gz") { + fileOp := files.NewFileOp() + if err := fileOp.TarGzExtractPro(file, dstDir, ""); err != nil { + _ = os.RemoveAll(dstDir) + return "", err + } + } + if strings.HasSuffix(fileName, ".zip") { + archiver, err := files.NewShellArchiver(files.Zip) + if err != nil { + _ = os.RemoveAll(dstDir) + return "", err + } + if err := archiver.Extract(file, dstDir, ""); err != nil { + _ = os.RemoveAll(dstDir) + return "", err + } + } + global.LOG.Infof("decompress file %s successful, now start to check test.sql is exist", file) + var sqlFiles []string + hasTestSql := false + _ = filepath.Walk(dstDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + if !info.IsDir() && strings.HasSuffix(info.Name(), ".sql") { + sqlFiles = append(sqlFiles, path) + if info.Name() == "test.sql" { + hasTestSql = true + } + } + return nil + }) + if len(sqlFiles) == 1 { + return sqlFiles[0], nil + } + if !hasTestSql { + _ = os.RemoveAll(dstDir) + return "", fmt.Errorf("no such file named test.sql in %s", fileName) + } + return "", nil +} diff --git a/agent/app/service/backup_postgresql.go b/agent/app/service/backup_postgresql.go index 747621d4a..ceae64124 100644 --- a/agent/app/service/backup_postgresql.go +++ b/agent/app/service/backup_postgresql.go @@ -4,8 +4,6 @@ import ( "fmt" "os" "path" - "path/filepath" - "strings" "time" "github.com/1Panel-dev/1Panel/agent/app/repo" @@ -59,44 +57,12 @@ func (u *BackupService) PostgresqlRecover(req dto.CommonRecover) error { } func (u *BackupService) PostgresqlRecoverByUpload(req dto.CommonRecover) error { - file := req.File - fileName := path.Base(req.File) - if strings.HasSuffix(fileName, ".tar.gz") { - fileNameItem := time.Now().Format(constant.DateTimeSlimLayout) - dstDir := fmt.Sprintf("%s/%s", path.Dir(req.File), fileNameItem) - fileOp := files.NewFileOp() - if !fileOp.Stat(dstDir) { - if err := fileOp.CreateDir(dstDir, os.ModePerm); err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", dstDir, err) - } - } - if err := fileOp.TarGzExtractPro(req.File, dstDir, ""); err != nil { - _ = os.RemoveAll(dstDir) - return err - } - global.LOG.Infof("decompress file %s successful, now start to check test.sql is exist", req.File) - hasTestSql := false - _ = filepath.Walk(dstDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if !info.IsDir() && info.Name() == "test.sql" { - hasTestSql = true - file = path - fileName = "test.sql" - } - return nil - }) - if !hasTestSql { - _ = os.RemoveAll(dstDir) - return fmt.Errorf("no such file named test.sql in %s", fileName) - } - defer func() { - _ = os.RemoveAll(dstDir) - }() + recoveFile, err := loadSqlFile(req.File) + if err != nil { + return err } - - req.File = path.Dir(file) + "/" + fileName + req.File = recoveFile + defer os.RemoveAll(path.Dir(recoveFile)) if err := handlePostgresqlRecover(req, nil, false); err != nil { return err } diff --git a/frontend/src/components/upload/index.vue b/frontend/src/components/upload/index.vue index 09aeef903..e8d607ee9 100644 --- a/frontend/src/components/upload/index.vue +++ b/frontend/src/components/upload/index.vue @@ -16,15 +16,9 @@
  • {{ $t('database.formatHelper', [remark]) }}
  • -
  • {{ $t('website.websiteBackupWarn') }}
  • - -
  • {{ $t('database.supportUpType') }}
  • -
  • {{ $t('database.zipFormat') }}
  • -
    - -
  • {{ $t('website.supportUpType') }}
  • -
  • {{ $t('website.zipFormat', [type + '.json']) }}
  • -
    +
  • {{ $t('database.supportUpType') }}
  • +
  • {{ $t('website.websiteBackupWarn') }}
  • +
  • {{ $t('website.supportUpType', [type]) }}
  • @@ -42,7 +36,7 @@ :limit="1" class="float-left" ref="uploadRef" - accept=".tar.gz,.sql,.sql.gz" + accept=".tar.gz,.sql,.gz,.zip" :show-file-list="false" :on-exceed="handleExceed" :on-change="fileOnChange" @@ -156,7 +150,7 @@ const paginationConfig = reactive({ total: 0, }); const uploadOpen = ref(false); -const type = ref(); +const type = ref('mysql'); const name = ref(); const detailName = ref(); const remark = ref(); @@ -208,15 +202,38 @@ const search = async () => { paginationConfig.total = res.data.total; }; +const beforeUpload = (fileName: string) => { + const itemName = fileName.toLowerCase(); + let reg = /^[a-zA-Z0-9\u4e00-\u9fa5]{1}[a-z:A-Z0-9_.\u4e00-\u9fa5-]{0,256}$/; + if (!reg.test(itemName)) { + MsgError(i18n.global.t('commons.msg.fileNameErr')); + return false; + } + if (isDb()) { + const allowedExtensions = ['.sql', '.sql.gz', '.tar.gz', '.zip']; + const isValidFile = allowedExtensions.some((ext) => itemName.endsWith(ext)); + if (!isValidFile) { + MsgError(i18n.global.t('database.supportUpType')); + return false; + } + return true; + } + const allowedExtensions = ['.tar.gz']; + const isValidFile = allowedExtensions.some((ext) => itemName.endsWith(ext)); + if (!isValidFile) { + MsgError(i18n.global.t('website.supportUpType')); + return false; + } + return true; +}; + const loadFile = async (path: string) => { let filaName = path.split('/').pop(); if (!filaName) { MsgError(i18n.global.t('commons.msg.fileNameErr')); return; } - let reg = /^[a-zA-Z0-9\u4e00-\u9fa5]{1}[a-z:A-Z0-9_.\u4e00-\u9fa5-]{0,256}$/; - if (!reg.test(filaName)) { - MsgError(i18n.global.t('commons.msg.fileNameErr')); + if (!beforeUpload(filaName)) { return; } ElMessageBox.confirm(i18n.global.t('database.selectHelper', [path]), i18n.global.t('database.loadBackup'), { @@ -297,6 +314,9 @@ const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => { MsgError(i18n.global.t('commons.msg.fileNameErr')); return; } + if (!beforeUpload(file.raw.name)) { + return; + } ElMessageBox.confirm( i18n.global.t('database.selectHelper', [file.raw.name]), i18n.global.t('database.loadBackup'), diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 34870d379..44beed4d3 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -521,8 +521,8 @@ const message = { selectFile: 'Select file', dropHelper: 'You can drag and drop the uploaded file here or', clickHelper: 'click to upload', - supportUpType: 'Only sql, sql.gz, and tar.gz files are supported', - zipFormat: 'tar.gz compressed package structure: test.tar.gz compressed package must contain test.sql', + supportUpType: + 'Only supports sql, sql.gz, tar.gz, .zip file formats. The imported compressed file must contain only one .sql file or include test.sql', currentStatus: 'Current state', baseParam: 'Basic parameter', @@ -2211,8 +2211,7 @@ const message = { otherDomains: 'Other domains', static: 'Static', deployment: 'Deployment', - supportUpType: 'Only .tar.gz files are supported', - zipFormat: '.tar.gz compressed package structure: test.tar.gz compressed package must contain {0} file', + supportUpType: 'Only .tar.gz file format is supported, and the compressed package must contain {0}.json file', proxy: 'Reverse proxy', alias: 'Alias', ftpUser: 'FTP account', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 5ffe5befb..89f44a4d0 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -509,8 +509,8 @@ const message = { selectFile: '[ファイル]を選択します', dropHelper: 'ここでアップロードされたファイルをドラッグアンドドロップするか、', clickHelper: 'クリックしてアップロードします', - supportUpType: 'SQL、SQL.GZ、およびTAR.GZファイルのみがサポートされています', - zipFormat: 'tar.gz圧縮パッケージ構造:test.tar.gz圧縮パッケージにはtest.sqlが含まれている必要があります', + supportUpType: + 'sql、sql.gz、tar.gz、.zip ファイル形式のみサポートしています。インポートする圧縮ファイルには、1つの.sqlファイルのみ、またはtest.sqlが含まれている必要があります', currentStatus: '現在の状態', baseParam: '基本パラメーター', @@ -2125,8 +2125,8 @@ const message = { otherDomains: '他のドメイン', static: '静的', deployment: '展開', - supportUpType: '.tar.gzファイルのみがサポートされています', - zipFormat: '.tar.gz圧縮パッケージ構造:test.tar.gz圧縮パッケージは{0}ファイルを含める必要があります', + supportUpType: + '.tar.gz ファイル形式のみサポートされており、圧縮パッケージには {0}.json ファイルが含まれている必要があります', proxy: '逆プロキシ', alias: 'エイリアス', ftpUser: 'FTPアカウント', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index a1bcf4f65..dd75093d8 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -507,8 +507,8 @@ const message = { selectFile: '파일 선택', dropHelper: '여기에 업로드한 파일을 드래그 앤 드롭하거나', clickHelper: '클릭하여 업로드', - supportUpType: 'sql, sql.gz, tar.gz 파일만 지원됩니다.', - zipFormat: 'tar.gz 압축 패키지 구조: test.tar.gz 압축 패키지에는 test.sql이 포함되어야 합니다.', + supportUpType: + 'sql, sql.gz, tar.gz, .zip 파일 형식만 지원합니다. 가져오는 압축 파일에는 하나의 .sql 파일만 있거나 test.sql이 포함되어 있어야 합니다', currentStatus: '현재 상태', baseParam: '기본 파라미터', @@ -2090,8 +2090,7 @@ const message = { otherDomains: '기타 도메인', static: '정적', deployment: '배포', - supportUpType: '지원되는 파일 형식: .tar.gz', - zipFormat: '.tar.gz 압축 패키지 구조: test.tar.gz 패키지에는 반드시 {0} 파일이 포함되어야 합니다.', + supportUpType: '.tar.gz 파일 형식만 지원되며, 압축 패키지에는 {0}.json 파일이 포함되어야 합니다', proxy: '리버스 프록시', alias: '별칭', ftpUser: 'FTP 계정', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index fd38bedd9..f8b64254b 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -521,8 +521,8 @@ const message = { selectFile: 'Pilih fail', dropHelper: 'Anda boleh seret dan lepaskan fail yang ingin dimuat naik di sini atau', clickHelper: 'klik untuk memuat naik', - supportUpType: 'Hanya fail sql, sql.gz, dan tar.gz yang disokong', - zipFormat: 'Struktur pakej mampatan tar.gz: Pakej mampatan test.tar.gz mesti mengandungi test.sql', + supportUpType: + 'Hanya menyokong format fail sql, sql.gz, tar.gz, .zip. Fail termampat yang diimport mesti mengandungi hanya satu fail .sql atau termasuk test.sql', currentStatus: 'Keadaan semasa', baseParam: 'Parameter asas', @@ -2181,8 +2181,7 @@ const message = { otherDomains: 'Domain Lain', static: 'Statik', deployment: 'Penerapan', - supportUpType: 'Hanya fail .tar.gz disokong', - zipFormat: 'Struktur fail .tar.gz: fail test.tar.gz mesti mengandungi fail {0}', + supportUpType: 'Hanya format fail .tar.gz yang disokong, dan pakej termampat mesti mengandungi fail {0}.json', proxy: 'Proksi Terbalik', alias: 'Alias', ftpUser: 'Akaun FTP', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 667380021..285413303 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -519,8 +519,8 @@ const message = { selectFile: 'Selecionar arquivo', dropHelper: 'Você pode arrastar e soltar o arquivo carregado aqui ou', clickHelper: 'clicar para fazer upload', - supportUpType: 'Apenas arquivos sql, sql.gz e tar.gz são suportados', - zipFormat: 'Estrutura do pacote comprimido tar.gz: o pacote comprimido test.tar.gz deve conter test.sql', + supportUpType: + 'Suporta apenas os formatos de arquivo sql, sql.gz, tar.gz, .zip. O arquivo compactado importado deve conter apenas um arquivo .sql ou incluir test.sql', currentStatus: 'Estado atual', baseParam: 'Parâmetro básico', @@ -2178,8 +2178,8 @@ const message = { otherDomains: 'Outros domínios', static: 'Estático', deployment: 'Implantação', - supportUpType: 'Somente arquivos .tar.gz são suportados', - zipFormat: 'Estrutura de pacote comprimido .tar.gz: o pacote comprimido test.tar.gz deve conter o arquivo {0}', + supportUpType: + 'Apenas o formato de arquivo .tar.gz é suportado, e o pacote compactado deve conter o arquivo {0}.json', proxy: 'Proxy reverso', alias: 'Alias', ftpUser: 'Conta FTP', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 9338e8133..dbfb3f44c 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -513,8 +513,8 @@ const message = { selectFile: 'Выбрать файл', dropHelper: 'Вы можете перетащить загружаемый файл сюда или', clickHelper: 'нажмите для загрузки', - supportUpType: 'Поддерживаются только файлы sql, sql.gz и tar.gz', - zipFormat: 'Структура архива tar.gz: архив test.tar.gz должен содержать файл test.sql', + supportUpType: + 'Поддерживаются только форматы файлов sql, sql.gz, tar.gz, .zip. Импортируемый сжатый файл должен содержать только один файл .sql или включать test.sql', currentStatus: 'Текущее состояние', baseParam: 'Базовые параметры', @@ -2174,8 +2174,7 @@ const message = { otherDomains: 'Другие домены', static: 'Статический', deployment: 'Развертывание', - supportUpType: 'Поддерживаются только файлы .tar.gz', - zipFormat: 'Структура архива .tar.gz: архив test.tar.gz должен содержать файл {0}', + supportUpType: 'Поддерживается только формат файла .tar.gz, и сжатый пакет должен содержать файл {0}.json', proxy: 'Обратный прокси', alias: 'Псевдоним', ftpUser: 'FTP аккаунт', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 3e9906e18..d6fdd4500 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -528,8 +528,8 @@ const message = { selectFile: 'Dosya seç', dropHelper: 'Yüklenen dosyayı buraya sürükleyip bırakabilir veya', clickHelper: 'yüklemek için tıklayın', - supportUpType: 'Yalnızca sql, sql.gz ve tar.gz dosyaları desteklenir', - zipFormat: 'tar.gz sıkıştırılmış paket yapısı: test.tar.gz sıkıştırılmış paketi test.sql içermelidir', + supportUpType: + 'Yalnızca sql, sql.gz, tar.gz, .zip dosya formatlarını destekler. İçe aktarılan sıkıştırılmış dosya yalnızca bir .sql dosyası içermeli veya test.sql içermelidir', currentStatus: 'Mevcut durum', baseParam: 'Temel parametre', @@ -2237,8 +2237,7 @@ const message = { otherDomains: 'Diğer alan adları', static: 'Statik', deployment: 'Dağıtım', - supportUpType: 'Yalnızca .tar.gz dosyaları desteklenir', - zipFormat: '.tar.gz sıkıştırılmış paket yapısı: test.tar.gz sıkıştırılmış paket {0} dosyasını içermelidir', + supportUpType: 'Yalnızca .tar.gz dosya formatı desteklenir ve sıkıştırılmış paket {0}.json dosyası içermelidir', proxy: 'Ters vekil', alias: 'Takma ad', ftpUser: 'FTP hesabı', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 688970605..41d332833 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -503,8 +503,8 @@ const message = { selectFile: '選擇文件', dropHelper: '將上傳文件拖拽到此處,或者', clickHelper: '點擊上傳', - supportUpType: '僅支持 sql、sql.gz、tar.gz 文件', - zipFormat: 'tar.gz 壓縮包結構:test.tar.gz 壓縮包內,必需包含 test.sql', + supportUpType: + '僅支持 sql、sql.gz、tar.gz、.zip 文件格式,導入的壓縮文件必須保證只有一個 .sql 文件或者包含 test.sql', currentStatus: '當前狀態', baseParam: '基礎參數', @@ -2059,8 +2059,7 @@ const message = { otherDomains: '其他域名', static: '靜態網站', deployment: '一鍵部署', - supportUpType: '僅支持 .tar.gz 文件', - zipFormat: '.tar.gz 壓縮包結構:test.tar.gz 壓縮包內,必需包含 {0} 文件', + supportUpType: '僅支持 .tar.gz 文件格式,且壓縮包內必須包含 {0}.json 文件', proxy: '反向代理', alias: '代號', ftpUser: 'FTP 帳號', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 96d6ee023..74af68eb5 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -501,8 +501,8 @@ const message = { selectFile: '选择文件', dropHelper: '将上传文件拖拽到此处,或者', clickHelper: '点击上传', - supportUpType: '仅支持 sql、sql.gz、tar.gz 文件', - zipFormat: 'tar.gz 压缩包结构:test.tar.gz 压缩包内,必需包含 test.sql', + supportUpType: + '仅支持 sql、sql.gz、tar.gz、.zip 文件格式,导入的压缩文件必须保证只有一个 .sql 文件或者包含 test.sql', currentStatus: '当前状态', baseParam: '基础参数', @@ -2049,8 +2049,7 @@ const message = { otherDomains: '其他域名', static: '静态网站', deployment: '一键部署', - supportUpType: '仅支持 .tar.gz 文件', - zipFormat: '.tar.gz 压缩包结构:test.tar.gz 压缩包内,必需包含 {0} 文件', + supportUpType: '仅支持 .tar.gz 文件格式,且压缩包内必须包含 {0}.json 文件', proxy: '反向代理', alias: '代号', ftpUser: 'FTP 账号',