feat: Add ZIP support for database backup uploads (#10246)

Refs #7179
This commit is contained in:
ssongliu 2025-09-03 13:59:28 +08:00 committed by GitHub
parent 7313f3d591
commit 789d451ea2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 125 additions and 126 deletions

View file

@ -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
}

View file

@ -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
}

View file

@ -16,15 +16,9 @@
<li v-if="type === 'mysql' || type === 'mariadb'">
{{ $t('database.formatHelper', [remark]) }}
</li>
<li v-if="type === 'website'">{{ $t('website.websiteBackupWarn') }}</li>
<span v-if="isDb()">
<li>{{ $t('database.supportUpType') }}</li>
<li>{{ $t('database.zipFormat') }}</li>
</span>
<span v-else>
<li>{{ $t('website.supportUpType') }}</li>
<li>{{ $t('website.zipFormat', [type + '.json']) }}</li>
</span>
<li v-if="isDb()">{{ $t('database.supportUpType') }}</li>
<li v-if="!isDb()">{{ $t('website.websiteBackupWarn') }}</li>
<li v-if="!isDb()">{{ $t('website.supportUpType', [type]) }}</li>
</ul>
</template>
</el-alert>
@ -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'),

View file

@ -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',

View file

@ -509,8 +509,8 @@ const message = {
selectFile: '[ファイル]を選択します',
dropHelper: 'ここでアップロードされたファイルをドラッグアンドドロップするか',
clickHelper: 'クリックしてアップロードします',
supportUpType: 'SQLSQL.GZおよびTAR.GZファイルのみがサポートされています',
zipFormat: 'tar.gz圧縮パッケージ構造:test.tar.gz圧縮パッケージにはtest.sqlが含まれている必要があります',
supportUpType:
'sqlsql.gztar.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アカウント',

View file

@ -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 계정',

View file

@ -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',

View file

@ -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',

View file

@ -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 аккаунт',

View file

@ -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ı',

View file

@ -503,8 +503,8 @@ const message = {
selectFile: '選擇文件',
dropHelper: '將上傳文件拖拽到此處或者',
clickHelper: '點擊上傳',
supportUpType: '僅支持 sqlsql.gztar.gz 文件',
zipFormat: 'tar.gz 壓縮包結構test.tar.gz 壓縮包內必需包含 test.sql',
supportUpType:
'僅支持 sqlsql.gztar.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 帳號',

View file

@ -501,8 +501,8 @@ const message = {
selectFile: '选择文件',
dropHelper: '将上传文件拖拽到此处或者',
clickHelper: '点击上传',
supportUpType: '仅支持 sqlsql.gztar.gz 文件',
zipFormat: 'tar.gz 压缩包结构test.tar.gz 压缩包内必需包含 test.sql',
supportUpType:
'仅支持 sqlsql.gztar.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 账号',