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 { func (u *BackupService) MysqlRecoverByUpload(req dto.CommonRecover) error {
file := req.File recoveFile, err := loadSqlFile(req.File)
fileName := path.Base(req.File) if err != nil {
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 return err
} }
global.LOG.Infof("decompress file %s successful, now start to check test.sql is exist", req.File) req.File = recoveFile
hasTestSql := false defer os.RemoveAll(path.Dir(recoveFile))
_ = 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)
}()
}
req.File = path.Dir(file) + "/" + fileName
if err := handleMysqlRecover(req, nil, false, req.TaskID); err != nil { if err := handleMysqlRecover(req, nil, false, req.TaskID); err != nil {
return err return err
} }
@ -243,3 +211,55 @@ func doMysqlBackup(db DatabaseHelper, targetDir, fileName string) error {
} }
return cli.Backup(backupInfo) 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" "fmt"
"os" "os"
"path" "path"
"path/filepath"
"strings"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/app/repo" "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 { func (u *BackupService) PostgresqlRecoverByUpload(req dto.CommonRecover) error {
file := req.File recoveFile, err := loadSqlFile(req.File)
fileName := path.Base(req.File) if err != nil {
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 return err
} }
global.LOG.Infof("decompress file %s successful, now start to check test.sql is exist", req.File) req.File = recoveFile
hasTestSql := false defer os.RemoveAll(path.Dir(recoveFile))
_ = 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)
}()
}
req.File = path.Dir(file) + "/" + fileName
if err := handlePostgresqlRecover(req, nil, false); err != nil { if err := handlePostgresqlRecover(req, nil, false); err != nil {
return err return err
} }

View file

@ -16,15 +16,9 @@
<li v-if="type === 'mysql' || type === 'mariadb'"> <li v-if="type === 'mysql' || type === 'mariadb'">
{{ $t('database.formatHelper', [remark]) }} {{ $t('database.formatHelper', [remark]) }}
</li> </li>
<li v-if="type === 'website'">{{ $t('website.websiteBackupWarn') }}</li> <li v-if="isDb()">{{ $t('database.supportUpType') }}</li>
<span v-if="isDb()"> <li v-if="!isDb()">{{ $t('website.websiteBackupWarn') }}</li>
<li>{{ $t('database.supportUpType') }}</li> <li v-if="!isDb()">{{ $t('website.supportUpType', [type]) }}</li>
<li>{{ $t('database.zipFormat') }}</li>
</span>
<span v-else>
<li>{{ $t('website.supportUpType') }}</li>
<li>{{ $t('website.zipFormat', [type + '.json']) }}</li>
</span>
</ul> </ul>
</template> </template>
</el-alert> </el-alert>
@ -42,7 +36,7 @@
:limit="1" :limit="1"
class="float-left" class="float-left"
ref="uploadRef" ref="uploadRef"
accept=".tar.gz,.sql,.sql.gz" accept=".tar.gz,.sql,.gz,.zip"
:show-file-list="false" :show-file-list="false"
:on-exceed="handleExceed" :on-exceed="handleExceed"
:on-change="fileOnChange" :on-change="fileOnChange"
@ -156,7 +150,7 @@ const paginationConfig = reactive({
total: 0, total: 0,
}); });
const uploadOpen = ref(false); const uploadOpen = ref(false);
const type = ref(); const type = ref('mysql');
const name = ref(); const name = ref();
const detailName = ref(); const detailName = ref();
const remark = ref(); const remark = ref();
@ -208,15 +202,38 @@ const search = async () => {
paginationConfig.total = res.data.total; 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) => { const loadFile = async (path: string) => {
let filaName = path.split('/').pop(); let filaName = path.split('/').pop();
if (!filaName) { if (!filaName) {
MsgError(i18n.global.t('commons.msg.fileNameErr')); MsgError(i18n.global.t('commons.msg.fileNameErr'));
return; return;
} }
let reg = /^[a-zA-Z0-9\u4e00-\u9fa5]{1}[a-z:A-Z0-9_.\u4e00-\u9fa5-]{0,256}$/; if (!beforeUpload(filaName)) {
if (!reg.test(filaName)) {
MsgError(i18n.global.t('commons.msg.fileNameErr'));
return; return;
} }
ElMessageBox.confirm(i18n.global.t('database.selectHelper', [path]), i18n.global.t('database.loadBackup'), { 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')); MsgError(i18n.global.t('commons.msg.fileNameErr'));
return; return;
} }
if (!beforeUpload(file.raw.name)) {
return;
}
ElMessageBox.confirm( ElMessageBox.confirm(
i18n.global.t('database.selectHelper', [file.raw.name]), i18n.global.t('database.selectHelper', [file.raw.name]),
i18n.global.t('database.loadBackup'), i18n.global.t('database.loadBackup'),

View file

@ -521,8 +521,8 @@ const message = {
selectFile: 'Select file', selectFile: 'Select file',
dropHelper: 'You can drag and drop the uploaded file here or', dropHelper: 'You can drag and drop the uploaded file here or',
clickHelper: 'click to upload', clickHelper: 'click to upload',
supportUpType: 'Only sql, sql.gz, and tar.gz files are supported', supportUpType:
zipFormat: 'tar.gz compressed package structure: test.tar.gz compressed package must contain test.sql', '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', currentStatus: 'Current state',
baseParam: 'Basic parameter', baseParam: 'Basic parameter',
@ -2211,8 +2211,7 @@ const message = {
otherDomains: 'Other domains', otherDomains: 'Other domains',
static: 'Static', static: 'Static',
deployment: 'Deployment', deployment: 'Deployment',
supportUpType: 'Only .tar.gz files are supported', supportUpType: 'Only .tar.gz file format is supported, and the compressed package must contain {0}.json file',
zipFormat: '.tar.gz compressed package structure: test.tar.gz compressed package must contain {0} file',
proxy: 'Reverse proxy', proxy: 'Reverse proxy',
alias: 'Alias', alias: 'Alias',
ftpUser: 'FTP account', ftpUser: 'FTP account',

View file

@ -509,8 +509,8 @@ const message = {
selectFile: '[ファイル]を選択します', selectFile: '[ファイル]を選択します',
dropHelper: 'ここでアップロードされたファイルをドラッグアンドドロップするか', dropHelper: 'ここでアップロードされたファイルをドラッグアンドドロップするか',
clickHelper: 'クリックしてアップロードします', clickHelper: 'クリックしてアップロードします',
supportUpType: 'SQLSQL.GZおよびTAR.GZファイルのみがサポートされています', supportUpType:
zipFormat: 'tar.gz圧縮パッケージ構造:test.tar.gz圧縮パッケージにはtest.sqlが含まれている必要があります', 'sqlsql.gztar.gz.zip ファイル形式のみサポートしていますインポートする圧縮ファイルには1つの.sqlファイルのみまたはtest.sqlが含まれている必要があります',
currentStatus: '現在の状態', currentStatus: '現在の状態',
baseParam: '基本パラメーター', baseParam: '基本パラメーター',
@ -2125,8 +2125,8 @@ const message = {
otherDomains: '他のドメイン', otherDomains: '他のドメイン',
static: '静的', static: '静的',
deployment: '展開', deployment: '展開',
supportUpType: '.tar.gzファイルのみがサポートされています', supportUpType:
zipFormat: '.tar.gz圧縮パッケージ構造:test.tar.gz圧縮パッケージは{0}ファイルを含める必要があります', '.tar.gz ファイル形式のみサポートされており圧縮パッケージには {0}.json ファイルが含まれている必要があります',
proxy: '逆プロキシ', proxy: '逆プロキシ',
alias: 'エイリアス', alias: 'エイリアス',
ftpUser: 'FTPアカウント', ftpUser: 'FTPアカウント',

View file

@ -507,8 +507,8 @@ const message = {
selectFile: '파일 선택', selectFile: '파일 선택',
dropHelper: '여기에 업로드한 파일을 드래그 드롭하거나', dropHelper: '여기에 업로드한 파일을 드래그 드롭하거나',
clickHelper: '클릭하여 업로드', clickHelper: '클릭하여 업로드',
supportUpType: 'sql, sql.gz, tar.gz 파일만 지원됩니다.', supportUpType:
zipFormat: 'tar.gz 압축 패키지 구조: test.tar.gz 압축 패키지에는 test.sql이 포함되어합니다.', 'sql, sql.gz, tar.gz, .zip 파일 형식만 지원합니다. 가져오는 압축 파일에는 하나의 .sql 파일만 있거나 test.sql이 포함되어 있어합니다',
currentStatus: '현재 상태', currentStatus: '현재 상태',
baseParam: '기본 파라미터', baseParam: '기본 파라미터',
@ -2090,8 +2090,7 @@ const message = {
otherDomains: '기타 도메인', otherDomains: '기타 도메인',
static: '정적', static: '정적',
deployment: '배포', deployment: '배포',
supportUpType: '지원되는 파일 형식: .tar.gz', supportUpType: '.tar.gz 파일 형식만 지원되며, 압축 패키지에는 {0}.json 파일이 포함되어야 합니다',
zipFormat: '.tar.gz 압축 패키지 구조: test.tar.gz 패키지에는 반드시 {0} 파일이 포함되어야 합니다.',
proxy: '리버스 프록시', proxy: '리버스 프록시',
alias: '별칭', alias: '별칭',
ftpUser: 'FTP 계정', ftpUser: 'FTP 계정',

View file

@ -521,8 +521,8 @@ const message = {
selectFile: 'Pilih fail', selectFile: 'Pilih fail',
dropHelper: 'Anda boleh seret dan lepaskan fail yang ingin dimuat naik di sini atau', dropHelper: 'Anda boleh seret dan lepaskan fail yang ingin dimuat naik di sini atau',
clickHelper: 'klik untuk memuat naik', clickHelper: 'klik untuk memuat naik',
supportUpType: 'Hanya fail sql, sql.gz, dan tar.gz yang disokong', supportUpType:
zipFormat: 'Struktur pakej mampatan tar.gz: Pakej mampatan test.tar.gz mesti mengandungi test.sql', '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', currentStatus: 'Keadaan semasa',
baseParam: 'Parameter asas', baseParam: 'Parameter asas',
@ -2181,8 +2181,7 @@ const message = {
otherDomains: 'Domain Lain', otherDomains: 'Domain Lain',
static: 'Statik', static: 'Statik',
deployment: 'Penerapan', deployment: 'Penerapan',
supportUpType: 'Hanya fail .tar.gz disokong', supportUpType: 'Hanya format fail .tar.gz yang disokong, dan pakej termampat mesti mengandungi fail {0}.json',
zipFormat: 'Struktur fail .tar.gz: fail test.tar.gz mesti mengandungi fail {0}',
proxy: 'Proksi Terbalik', proxy: 'Proksi Terbalik',
alias: 'Alias', alias: 'Alias',
ftpUser: 'Akaun FTP', ftpUser: 'Akaun FTP',

View file

@ -519,8 +519,8 @@ const message = {
selectFile: 'Selecionar arquivo', selectFile: 'Selecionar arquivo',
dropHelper: 'Você pode arrastar e soltar o arquivo carregado aqui ou', dropHelper: 'Você pode arrastar e soltar o arquivo carregado aqui ou',
clickHelper: 'clicar para fazer upload', clickHelper: 'clicar para fazer upload',
supportUpType: 'Apenas arquivos sql, sql.gz e tar.gz são suportados', supportUpType:
zipFormat: 'Estrutura do pacote comprimido tar.gz: o pacote comprimido test.tar.gz deve conter test.sql', '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', currentStatus: 'Estado atual',
baseParam: 'Parâmetro básico', baseParam: 'Parâmetro básico',
@ -2178,8 +2178,8 @@ const message = {
otherDomains: 'Outros domínios', otherDomains: 'Outros domínios',
static: 'Estático', static: 'Estático',
deployment: 'Implantação', deployment: 'Implantação',
supportUpType: 'Somente arquivos .tar.gz são suportados', supportUpType:
zipFormat: 'Estrutura de pacote comprimido .tar.gz: o pacote comprimido test.tar.gz deve conter o arquivo {0}', 'Apenas o formato de arquivo .tar.gz é suportado, e o pacote compactado deve conter o arquivo {0}.json',
proxy: 'Proxy reverso', proxy: 'Proxy reverso',
alias: 'Alias', alias: 'Alias',
ftpUser: 'Conta FTP', ftpUser: 'Conta FTP',

View file

@ -513,8 +513,8 @@ const message = {
selectFile: 'Выбрать файл', selectFile: 'Выбрать файл',
dropHelper: 'Вы можете перетащить загружаемый файл сюда или', dropHelper: 'Вы можете перетащить загружаемый файл сюда или',
clickHelper: 'нажмите для загрузки', clickHelper: 'нажмите для загрузки',
supportUpType: 'Поддерживаются только файлы sql, sql.gz и tar.gz', supportUpType:
zipFormat: 'Структура архива tar.gz: архив test.tar.gz должен содержать файл test.sql', 'Поддерживаются только форматы файлов sql, sql.gz, tar.gz, .zip. Импортируемый сжатый файл должен содержать только один файл .sql или включать test.sql',
currentStatus: 'Текущее состояние', currentStatus: 'Текущее состояние',
baseParam: 'Базовые параметры', baseParam: 'Базовые параметры',
@ -2174,8 +2174,7 @@ const message = {
otherDomains: 'Другие домены', otherDomains: 'Другие домены',
static: 'Статический', static: 'Статический',
deployment: 'Развертывание', deployment: 'Развертывание',
supportUpType: 'Поддерживаются только файлы .tar.gz', supportUpType: 'Поддерживается только формат файла .tar.gz, и сжатый пакет должен содержать файл {0}.json',
zipFormat: 'Структура архива .tar.gz: архив test.tar.gz должен содержать файл {0}',
proxy: 'Обратный прокси', proxy: 'Обратный прокси',
alias: 'Псевдоним', alias: 'Псевдоним',
ftpUser: 'FTP аккаунт', ftpUser: 'FTP аккаунт',

View file

@ -528,8 +528,8 @@ const message = {
selectFile: 'Dosya seç', selectFile: 'Dosya seç',
dropHelper: 'Yüklenen dosyayı buraya sürükleyip bırakabilir veya', dropHelper: 'Yüklenen dosyayı buraya sürükleyip bırakabilir veya',
clickHelper: 'yüklemek için tıklayın', clickHelper: 'yüklemek için tıklayın',
supportUpType: 'Yalnızca sql, sql.gz ve tar.gz dosyaları desteklenir', supportUpType:
zipFormat: 'tar.gz sıkıştırılmış paket yapısı: test.tar.gz sıkıştırılmış paketi test.sql içermelidir', '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', currentStatus: 'Mevcut durum',
baseParam: 'Temel parametre', baseParam: 'Temel parametre',
@ -2237,8 +2237,7 @@ const message = {
otherDomains: 'Diğer alan adları', otherDomains: 'Diğer alan adları',
static: 'Statik', static: 'Statik',
deployment: 'Dağıtım', deployment: 'Dağıtım',
supportUpType: 'Yalnızca .tar.gz dosyaları desteklenir', supportUpType: 'Yalnızca .tar.gz dosya formatı desteklenir ve sıkıştırılmış paket {0}.json dosyası içermelidir',
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',
proxy: 'Ters vekil', proxy: 'Ters vekil',
alias: 'Takma ad', alias: 'Takma ad',
ftpUser: 'FTP hesabı', ftpUser: 'FTP hesabı',

View file

@ -503,8 +503,8 @@ const message = {
selectFile: '選擇文件', selectFile: '選擇文件',
dropHelper: '將上傳文件拖拽到此處或者', dropHelper: '將上傳文件拖拽到此處或者',
clickHelper: '點擊上傳', clickHelper: '點擊上傳',
supportUpType: '僅支持 sqlsql.gztar.gz 文件', supportUpType:
zipFormat: 'tar.gz 壓縮包結構test.tar.gz 壓縮包內必需包含 test.sql', '僅支持 sqlsql.gztar.gz.zip 文件格式導入的壓縮文件必須保證只有一個 .sql 文件或者包含 test.sql',
currentStatus: '當前狀態', currentStatus: '當前狀態',
baseParam: '基礎參數', baseParam: '基礎參數',
@ -2059,8 +2059,7 @@ const message = {
otherDomains: '其他域名', otherDomains: '其他域名',
static: '靜態網站', static: '靜態網站',
deployment: '一鍵部署', deployment: '一鍵部署',
supportUpType: '僅支持 .tar.gz 文件', supportUpType: '僅支持 .tar.gz 文件格式且壓縮包內必須包含 {0}.json 文件',
zipFormat: '.tar.gz 壓縮包結構test.tar.gz 壓縮包內必需包含 {0} 文件',
proxy: '反向代理', proxy: '反向代理',
alias: '代號', alias: '代號',
ftpUser: 'FTP 帳號', ftpUser: 'FTP 帳號',

View file

@ -501,8 +501,8 @@ const message = {
selectFile: '选择文件', selectFile: '选择文件',
dropHelper: '将上传文件拖拽到此处或者', dropHelper: '将上传文件拖拽到此处或者',
clickHelper: '点击上传', clickHelper: '点击上传',
supportUpType: '仅支持 sqlsql.gztar.gz 文件', supportUpType:
zipFormat: 'tar.gz 压缩包结构test.tar.gz 压缩包内必需包含 test.sql', '仅支持 sqlsql.gztar.gz.zip 文件格式导入的压缩文件必须保证只有一个 .sql 文件或者包含 test.sql',
currentStatus: '当前状态', currentStatus: '当前状态',
baseParam: '基础参数', baseParam: '基础参数',
@ -2049,8 +2049,7 @@ const message = {
otherDomains: '其他域名', otherDomains: '其他域名',
static: '静态网站', static: '静态网站',
deployment: '一键部署', deployment: '一键部署',
supportUpType: '仅支持 .tar.gz 文件', supportUpType: '仅支持 .tar.gz 文件格式且压缩包内必须包含 {0}.json 文件',
zipFormat: '.tar.gz 压缩包结构test.tar.gz 压缩包内必需包含 {0} 文件',
proxy: '反向代理', proxy: '反向代理',
alias: '代号', alias: '代号',
ftpUser: 'FTP 账号', ftpUser: 'FTP 账号',