mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-20 16:00:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cdc4f476a |
@@ -212,17 +212,10 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
websiteSSL.Shell = req.Shell
|
||||
}
|
||||
if req.PushDir {
|
||||
dirs := certificateDirectories(req.Dir)
|
||||
if len(dirs) == 0 {
|
||||
if !files.NewFileOp().Stat(req.Dir) {
|
||||
return nil, buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil || !info.IsDir() {
|
||||
return nil, buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
}
|
||||
websiteSSL.Dir = strings.Join(dirs, "\n")
|
||||
websiteSSL.Dir = req.Dir
|
||||
}
|
||||
if req.Domains != "" {
|
||||
domainArray := strings.Split(req.Domains, "\n")
|
||||
@@ -355,9 +348,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
if websiteSSL.ExecShell {
|
||||
workDir := global.Dir.DataDir
|
||||
if websiteSSL.PushDir {
|
||||
if dirs := certificateDirectories(websiteSSL.Dir); len(dirs) > 0 {
|
||||
workDir = dirs[0]
|
||||
}
|
||||
workDir = websiteSSL.Dir
|
||||
}
|
||||
logger.Println(i18n.GetMsgByKey("ExecShellStart"))
|
||||
if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil {
|
||||
|
||||
@@ -209,17 +209,11 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
|
||||
websiteSSL.Shell = create.Shell
|
||||
}
|
||||
if create.PushDir {
|
||||
dirs := certificateDirectories(create.Dir)
|
||||
if len(dirs) == 0 {
|
||||
fileOP := files.NewFileOp()
|
||||
if !fileOP.Stat(create.Dir) {
|
||||
return res, buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil || !info.IsDir() {
|
||||
return res, buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
}
|
||||
websiteSSL.Dir = strings.Join(dirs, "\n")
|
||||
websiteSSL.Dir = create.Dir
|
||||
}
|
||||
setSSLPushConfig(&websiteSSL, create.PushNode, create.Nodes)
|
||||
|
||||
@@ -579,9 +573,7 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
|
||||
if websiteSSL.ExecShell {
|
||||
workDir := global.Dir.DataDir
|
||||
if websiteSSL.PushDir {
|
||||
if dirs := certificateDirectories(websiteSSL.Dir); len(dirs) > 0 {
|
||||
workDir = dirs[0]
|
||||
}
|
||||
workDir = websiteSSL.Dir
|
||||
}
|
||||
printSSLLog(logger, "ExecShellStart", nil)
|
||||
if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil {
|
||||
@@ -801,16 +793,11 @@ func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error {
|
||||
}
|
||||
|
||||
if update.PushDir {
|
||||
dirs := certificateDirectories(update.Dir)
|
||||
if len(dirs) == 0 {
|
||||
return buserr.New("ErrLinkPathNotFound")
|
||||
fileOP := files.NewFileOp()
|
||||
if !fileOP.Stat(update.Dir) {
|
||||
_ = fileOP.CreateDir(update.Dir, constant.DirPerm)
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
updateParams["dir"] = strings.Join(dirs, "\n")
|
||||
updateParams["dir"] = update.Dir
|
||||
}
|
||||
var domains []string
|
||||
if update.OtherDomains != "" {
|
||||
|
||||
@@ -1529,41 +1529,26 @@ func checkWebsitePort(defaultHTTPsPort, port int, websiteType string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// certificateDirectories accepts both legacy single paths and newline-separated paths.
|
||||
func certificateDirectories(value string) []string {
|
||||
var dirs []string
|
||||
seen := make(map[string]bool)
|
||||
for _, dir := range strings.Split(value, "\n") {
|
||||
dir = strings.TrimSpace(dir)
|
||||
if dir == "" {
|
||||
continue
|
||||
}
|
||||
dir = filepath.Clean(dir)
|
||||
if !seen[dir] {
|
||||
seen[dir] = true
|
||||
dirs = append(dirs, dir)
|
||||
}
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
func saveCertificateFile(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
|
||||
if !websiteSSL.PushDir {
|
||||
return
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
for _, dir := range certificateDirectories(websiteSSL.Dir) {
|
||||
status := i18n.GetMsgByKey("Success")
|
||||
for _, file := range []struct{ name, content string }{
|
||||
{"privkey.pem", websiteSSL.PrivateKey},
|
||||
{"fullchain.pem", websiteSSL.Pem},
|
||||
} {
|
||||
if err := fileOp.SaveFile(path.Join(dir, file.name), file.content, constant.FilePerm); err != nil {
|
||||
status = i18n.GetMsgByKey("Failed")
|
||||
logger.Printf("Push dir %s failed: %s", dir, err)
|
||||
}
|
||||
if websiteSSL.PushDir {
|
||||
fileOp := files.NewFileOp()
|
||||
var (
|
||||
pushErr error
|
||||
MsgMap = map[string]interface{}{"path": websiteSSL.Dir, "status": i18n.GetMsgByKey("Success")}
|
||||
)
|
||||
if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "privkey.pem"), websiteSSL.PrivateKey, constant.FilePerm); pushErr != nil {
|
||||
MsgMap["status"] = i18n.GetMsgByKey("Failed")
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
|
||||
logger.Println("Push dir failed:" + pushErr.Error())
|
||||
}
|
||||
if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "fullchain.pem"), websiteSSL.Pem, constant.FilePerm); pushErr != nil {
|
||||
MsgMap["status"] = i18n.GetMsgByKey("Failed")
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
|
||||
logger.Println("Push dir failed:" + pushErr.Error())
|
||||
}
|
||||
if pushErr == nil {
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap))
|
||||
}
|
||||
logger.Println(i18n.GetMsgWithMap("PushDirLog", map[string]interface{}{"path": dir, "status": status}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import http from '@/api';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { ResPage, SearchWithPage } from '../interface';
|
||||
import { Container } from '../interface/container';
|
||||
import { TimeoutEnum } from '@/enums/http-enum';
|
||||
@@ -23,10 +24,11 @@ export const loadContainerUsers = (name: string) => {
|
||||
export const listContainerFiles = (params: Container.ContainerFileReq) => {
|
||||
return http.post<Array<Container.ContainerFileInfo>>(`/containers/files/search`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const uploadContainerFile = (params: FormData) => {
|
||||
export const uploadContainerFile = (params: FormData, config?: AxiosRequestConfig & { skipErrorMessage?: boolean }) => {
|
||||
return http.upload(`/containers/files/upload`, params, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: TimeoutEnum.T_5M,
|
||||
...config,
|
||||
headers: { 'Content-Type': 'multipart/form-data', ...config?.headers },
|
||||
});
|
||||
};
|
||||
export const getContainerFileContent = (params: Container.ContainerFileReq) => {
|
||||
|
||||
@@ -79,16 +79,6 @@
|
||||
@row-click="handleRowClick"
|
||||
class="cursor-pointer dialog-file-list"
|
||||
>
|
||||
<el-table-column v-if="isMultiple && form.dir" width="48">
|
||||
<template #default="{ row }">
|
||||
<el-checkbox
|
||||
v-if="row.isDir && !row.isCreate"
|
||||
:model-value="selectedPathSet.has(row.path)"
|
||||
@click.stop
|
||||
@change="toggleDirectory(row.path)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" show-overflow-tooltip fix>
|
||||
<template #default="{ row }">
|
||||
<svg-icon
|
||||
@@ -141,13 +131,6 @@
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="file-list-bottom">
|
||||
<el-checkbox
|
||||
v-if="isMultiple && form.dir"
|
||||
:model-value="selectedPathSet.has(req.path)"
|
||||
@change="toggleDirectory(req.path)"
|
||||
>
|
||||
{{ req.path }}
|
||||
</el-checkbox>
|
||||
<div v-if="!isMultiple && selectRow?.path">
|
||||
{{ $t('file.currentSelect') }}
|
||||
<el-tooltip :content="selectRow.path" placement="top">
|
||||
@@ -180,11 +163,7 @@
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="selectFile"
|
||||
:disabled="disBtn || (isMultiple && selectRows.length === 0)"
|
||||
>
|
||||
<el-button type="primary" @click="selectFile" :disabled="disBtn">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
@@ -195,7 +174,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { File } from '@/api/interface/file';
|
||||
import { computeDirSize, createFile, getFileContent, getFilesList, searchFavorite } from '@/api/modules/files';
|
||||
import { computed, nextTick, reactive, ref } from 'vue';
|
||||
import { nextTick, onUpdated, reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess, MsgWarning } from '@/utils/message';
|
||||
import { useSearchableForSelect } from '@/views/host/file-management/hooks/searchable';
|
||||
@@ -207,7 +186,6 @@ const paths = ref<string[]>([]);
|
||||
const req = reactive({ path: '/', expand: true, page: 1, pageSize: 300, showHidden: true });
|
||||
const selectRow = ref({ path: '', name: '' });
|
||||
const selectRows = ref<Array<{ path: string; name: string }>>([]);
|
||||
const selectedPathSet = computed(() => new Set(selectRows.value.map((item) => item.path)));
|
||||
const isMultiple = ref(false);
|
||||
const rowRefs = ref();
|
||||
const open = ref(false);
|
||||
@@ -233,7 +211,6 @@ interface DialogProps {
|
||||
isAll?: boolean;
|
||||
disabled?: boolean;
|
||||
multiple?: boolean;
|
||||
selectedPaths?: string[];
|
||||
}
|
||||
const acceptParams = (props: DialogProps): void => {
|
||||
form.path = props.path || '/';
|
||||
@@ -241,8 +218,7 @@ const acceptParams = (props: DialogProps): void => {
|
||||
form.isAll = props.isAll || false;
|
||||
form.disabled = props.disabled || false;
|
||||
isMultiple.value = props.multiple || false;
|
||||
disBtn.value = false;
|
||||
selectRows.value = [...new Set(props.selectedPaths || [])].map((path) => ({ path, name: path }));
|
||||
selectRows.value = [];
|
||||
selectRow.value = { path: '', name: '' };
|
||||
openPage();
|
||||
req.path = form.path;
|
||||
@@ -305,7 +281,7 @@ const openDir = async (row: File.File, column: any, event: any) => {
|
||||
}
|
||||
// Handle file click
|
||||
const fullPath = (req.path === '/' ? req.path : req.path + '/') + row.name;
|
||||
if (isMultiple.value && (form.isAll || !form.dir)) {
|
||||
if (isMultiple.value) {
|
||||
// Multiple mode: toggle selection
|
||||
const index = selectRows.value.findIndex((r) => r.path === fullPath);
|
||||
if (index > -1) {
|
||||
@@ -497,25 +473,22 @@ const createFolder = async (row: any) => {
|
||||
Object.assign(addItem, addForm);
|
||||
loading.value = true;
|
||||
createFile(addItem as File.FileCreate)
|
||||
.then(async () => {
|
||||
.then(() => {
|
||||
row.isCreate = false;
|
||||
disBtn.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
|
||||
await search(req);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleDirectory = (path: string) => {
|
||||
const index = selectRows.value.findIndex((item) => item.path === path);
|
||||
if (index >= 0) {
|
||||
selectRows.value.splice(index, 1);
|
||||
} else {
|
||||
selectRows.value.push({ path, name: path });
|
||||
onUpdated(() => {
|
||||
if (form.path != '') {
|
||||
req.path = form.path;
|
||||
}
|
||||
};
|
||||
search(req);
|
||||
});
|
||||
|
||||
const removeSelected = (index: number) => {
|
||||
selectRows.value.splice(index, 1);
|
||||
|
||||
@@ -4252,7 +4252,7 @@ const message = {
|
||||
pushDir: 'Push the certificate to the local directory',
|
||||
dir: 'Directory',
|
||||
pushDirHelper:
|
||||
'Select multiple directories or enter one directory per line. Each directory will receive "fullchain.pem" and "privkey.pem".',
|
||||
'Certificate file "fullchain.pem" and key file "privkey.pem" will be generated in this directory.',
|
||||
organizationDetail: 'Organization details',
|
||||
fromWebsite: 'From website',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -4301,8 +4301,7 @@ const message = {
|
||||
domainHelper: 'Un dominio por línea, admite * y dirección IP',
|
||||
pushDir: 'Guardar certificado en el directorio local',
|
||||
dir: 'Directorio',
|
||||
pushDirHelper:
|
||||
'Seleccione varios directorios o introduzca un directorio por línea. En cada directorio se generarán los archivos "fullchain.pem" y "privkey.pem".',
|
||||
pushDirHelper: 'Se generarán los archivos "fullchain.pem" y "privkey.pem" en este directorio.',
|
||||
organizationDetail: 'Detalles de la organización',
|
||||
fromWebsite: 'Desde el sitio web',
|
||||
dnsMauanlHelper: 'En modo manual, haz clic en solicitar tras la creación para obtener el valor DNS',
|
||||
|
||||
@@ -4214,8 +4214,7 @@ const message = {
|
||||
domainHelper: 'هر خط یک نام دامنه، پشتیبانی از * و آدرس IP',
|
||||
pushDir: 'ارسال گواهی به دایرکتوری محلی',
|
||||
dir: 'دایرکتوری',
|
||||
pushDirHelper:
|
||||
'چند دایرکتوری انتخاب کنید یا در هر خط یک دایرکتوری وارد کنید. فایلهای "fullchain.pem" و "privkey.pem" در هر دایرکتوری ایجاد میشوند.',
|
||||
pushDirHelper: 'فایل گواهی "fullchain.pem" و فایل کلید "privkey.pem" در این دایرکتوری تولید میشوند.',
|
||||
organizationDetail: 'جزئیات سازمان',
|
||||
fromWebsite: 'از وبسایت',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -4237,8 +4237,7 @@ const message = {
|
||||
domainHelper: '1行ごとに1つのドメイン名、 *およびIPアドレスをサポートします',
|
||||
pushDir: '証明書をローカルディレクトリにプッシュします',
|
||||
dir: 'ディレクトリ',
|
||||
pushDirHelper:
|
||||
'複数のディレクトリを選択するか、1行に1つずつディレクトリを入力してください。各ディレクトリに「fullchain.pem」と「privkey.pem」が生成されます。',
|
||||
pushDirHelper: '証明書ファイル「FullChain.PEM」とキーファイル「Privkey.Pem」がこのディレクトリで生成されます。',
|
||||
organizationDetail: '組織の詳細',
|
||||
fromWebsite: 'ウェブサイトから',
|
||||
dnsMauanlHelper: '手動解像度モードでは、作成後に適用ボタンをクリックしてDNS解像度値を取得する必要があります',
|
||||
|
||||
@@ -4159,8 +4159,7 @@ const message = {
|
||||
domainHelper: '줄당 하나의 도메인 이름, * 및 IP 주소 지원',
|
||||
pushDir: '로컬 디렉토리로 인증서 푸시',
|
||||
dir: '디렉토리',
|
||||
pushDirHelper:
|
||||
'여러 디렉터리를 선택하거나 한 줄에 하나씩 입력하세요. 각 디렉터리에 "fullchain.pem"과 "privkey.pem" 파일이 생성됩니다.',
|
||||
pushDirHelper: '인증서 파일 "fullchain.pem" 및 키 파일 "privkey.pem"이 이 디렉토리에 생성됩니다.',
|
||||
organizationDetail: '조직 세부 정보',
|
||||
fromWebsite: '웹사이트에서 가져오기',
|
||||
dnsMauanlHelper: '수동 해석 모드에서는 생성 후 신청 버튼을 클릭하여 DNS 해석 값을 얻어야 합니다.',
|
||||
|
||||
@@ -4131,8 +4131,7 @@ const message = {
|
||||
domainHelper: 'ໜຶ່ງໂດເມນຕໍ່ແຖວ, ຮອງຮັບ * ແລະ ທີ່ຢູ່ IP',
|
||||
pushDir: 'ສົ່ງໃບຢັ້ງຢືນໄປຍັງໂຟນເດີໃນເຄື່ອງ',
|
||||
dir: 'ໂຟນເດີ',
|
||||
pushDirHelper:
|
||||
'ເລືອກຫຼາຍໂຟນເດີ ຫຼື ປ້ອນໜຶ່ງໂຟນເດີຕໍ່ແຖວ. ໄຟລ໌ "fullchain.pem" ແລະ "privkey.pem" ຈະຖືກສ້າງໃນແຕ່ລະໂຟນເດີ.',
|
||||
pushDirHelper: 'ໄຟລ໌ "fullchain.pem" ແລະ "privkey.pem" ຈະຖືກສ້າງໃນໂຟນເດີນີ້.',
|
||||
organizationDetail: 'ລາຍລະອຽດອົງກອນ',
|
||||
fromWebsite: 'ຈາກເວັບໄຊ',
|
||||
dnsMauanlHelper: 'ໃນໂໝດຈັດການ DNS ດ້ວຍຕົນເອງ, ທ່ານຕ້ອງຄລິກປຸ່ມ "ຂໍໃບຢັ້ງຢືນ" ຫຼັງຈາກສ້າງເພື່ອຮັບຄ່າ DNS',
|
||||
|
||||
@@ -4315,8 +4315,7 @@ const message = {
|
||||
domainHelper: 'Satu nama domain setiap baris, menyokong * dan alamat IP',
|
||||
pushDir: 'Tolakkan sijil ke direktori tempatan',
|
||||
dir: 'Direktori',
|
||||
pushDirHelper:
|
||||
'Pilih beberapa direktori atau masukkan satu direktori pada setiap baris. Fail "fullchain.pem" dan "privkey.pem" akan dihasilkan dalam setiap direktori.',
|
||||
pushDirHelper: 'Fail sijil "fullchain.pem" dan fail kunci "privkey.pem" akan dihasilkan dalam direktori ini.',
|
||||
organizationDetail: 'Butiran organisasi',
|
||||
fromWebsite: 'Daripada laman web',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -4335,7 +4335,7 @@ const message = {
|
||||
pushDir: 'Enviar o certificado para o diretório local',
|
||||
dir: 'Diretório',
|
||||
pushDirHelper:
|
||||
'Selecione vários diretórios ou informe um diretório por linha. Os arquivos "fullchain.pem" e "privkey.pem" serão gerados em cada diretório.',
|
||||
'O arquivo do certificado "fullchain.pem" e o arquivo de chave "privkey.pem" serão gerados neste diretório.',
|
||||
organizationDetail: 'Detalhes da organização',
|
||||
fromWebsite: 'Do site',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -4302,8 +4302,7 @@ const message = {
|
||||
domainHelper: 'Одно доменное имя в строке, поддерживает * и IP-адрес',
|
||||
pushDir: 'Отправить сертификат в локальную директорию',
|
||||
dir: 'Директория',
|
||||
pushDirHelper:
|
||||
'Выберите несколько директорий или укажите по одной директории в строке. В каждой директории будут созданы файлы "fullchain.pem" и "privkey.pem".',
|
||||
pushDirHelper: 'В этой директории будут созданы файл сертификата "fullchain.pem" и файл ключа "privkey.pem".',
|
||||
organizationDetail: 'Детали организации',
|
||||
fromWebsite: 'С веб-сайта',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -4321,7 +4321,7 @@ const message = {
|
||||
pushDir: 'Sertifikayı yerel dizine aktar',
|
||||
dir: 'Dizin',
|
||||
pushDirHelper:
|
||||
'Birden fazla dizin seçin veya her satıra bir dizin girin. Her dizinde "fullchain.pem" ve "privkey.pem" dosyaları oluşturulacaktır.',
|
||||
'Sertifika dosyası "fullchain.pem" ve anahtar dosyası "privkey.pem" bu dizinde oluşturulacaktır.',
|
||||
organizationDetail: 'Organizasyon detayları',
|
||||
fromWebsite: 'Web sitesinden',
|
||||
dnsMauanlHelper:
|
||||
|
||||
@@ -3970,7 +3970,7 @@ const message = {
|
||||
domainHelper: '一行一個網域名稱,支援*和IP位址',
|
||||
pushDir: '推送憑證到本機目錄',
|
||||
dir: '目錄',
|
||||
pushDirHelper: '支援選擇多個目錄,也可每行填寫一個目錄。每個目錄下都會產生 fullchain.pem 和 privkey.pem。',
|
||||
pushDirHelper: '會在此目錄下產生兩個檔案,憑證檔案:fullchain.pem 金鑰檔案:privkey.pem',
|
||||
organizationDetail: '機構詳情',
|
||||
fromWebsite: '從網站取得',
|
||||
dnsMauanlHelper: '手動解析模式需要在建立完之後點選申請按鈕取得 DNS 解析值',
|
||||
|
||||
@@ -4015,8 +4015,7 @@ const message = {
|
||||
domainHelper: '一行一个域名,支持*和IP地址',
|
||||
pushDir: '推送证书到本地目录',
|
||||
dir: '目录',
|
||||
pushDirHelper:
|
||||
'支持选择多个目录,也可每行填写一个目录。每个目录下都会生成证书文件 fullchain.pem 和密钥文件 privkey.pem',
|
||||
pushDirHelper: '会在此目录下生成两个文件,证书文件:fullchain.pem 密钥文件:privkey.pem',
|
||||
organizationDetail: '机构详情',
|
||||
fromWebsite: '从网站中获取',
|
||||
dnsMauanlHelper: '手动解析模式需要在创建完之后点击申请按钮获取 DNS 解析值',
|
||||
|
||||
@@ -32,11 +32,9 @@
|
||||
@blur="cancelPathEditing"
|
||||
@keyup.enter.prevent="submitPathInput"
|
||||
/>
|
||||
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" :on-change="onUploadChange">
|
||||
<el-button class="mt-2" :loading="uploading" type="primary" plain>
|
||||
{{ $t('commons.button.upload') }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<el-button class="mt-2" type="primary" plain @click="onUpload">
|
||||
{{ $t('commons.button.upload') }}
|
||||
</el-button>
|
||||
<el-button class="mt-2 ml-3" plain :disabled="selectedRows.length === 0" @click="onBatchDelete">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
@@ -97,6 +95,7 @@
|
||||
</el-form>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
<FileUpload ref="uploadRef" @close="loadContainerFiles" />
|
||||
<DialogPro v-model="previewVisible" :title="previewTitle" size="w-70" :close-on-click-modal="true">
|
||||
<el-alert
|
||||
v-if="previewTruncated"
|
||||
@@ -118,11 +117,11 @@ import {
|
||||
getContainerFileContent,
|
||||
getContainerFileSize,
|
||||
listContainerFiles,
|
||||
uploadContainerFile,
|
||||
} from '@/api/modules/container';
|
||||
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
import { ElInput, ElMessageBox, UploadFile, UploadInstance } from 'element-plus';
|
||||
import { ElInput, ElMessageBox } from 'element-plus';
|
||||
import FileUpload from '@/views/host/file-management/upload/index.vue';
|
||||
import { Document, FolderOpened, HomeFilled } from '@element-plus/icons-vue';
|
||||
import { computeSize2 } from '@/utils/size';
|
||||
const visible = ref(false);
|
||||
@@ -130,9 +129,8 @@ const title = ref('');
|
||||
const containerID = ref('');
|
||||
const filePath = ref('/');
|
||||
const containerFiles = ref<any[]>([]);
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
const uploadRef = ref<InstanceType<typeof FileUpload>>();
|
||||
const pathInputRef = ref<InstanceType<typeof ElInput>>();
|
||||
const uploading = ref(false);
|
||||
const previewVisible = ref(false);
|
||||
const previewTitle = ref('');
|
||||
const previewContent = ref('');
|
||||
@@ -253,27 +251,8 @@ const submitPathInput = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onUploadChange = async (uploadFile: UploadFile) => {
|
||||
if (!uploadFile.raw) {
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('containerID', containerID.value);
|
||||
formData.append('path', filePath.value);
|
||||
formData.append('file', uploadFile.raw);
|
||||
uploading.value = true;
|
||||
await uploadContainerFile(formData)
|
||||
.then(async () => {
|
||||
MsgSuccess(i18n.global.t('file.uploadSuccess'));
|
||||
await loadContainerFiles();
|
||||
})
|
||||
.catch((err) => {
|
||||
MsgError(err?.message || i18n.global.t('commons.msg.operationFailed'));
|
||||
})
|
||||
.finally(() => {
|
||||
uploading.value = false;
|
||||
uploadRef.value?.clearFiles();
|
||||
});
|
||||
const onUpload = () => {
|
||||
uploadRef.value?.acceptParams({ path: filePath.value, containerID: containerID.value });
|
||||
};
|
||||
|
||||
const onOpenFile = async (path: string) => {
|
||||
|
||||
@@ -119,8 +119,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, ref } from 'vue';
|
||||
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
|
||||
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
|
||||
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile, genFileId } from 'element-plus';
|
||||
import { uploadContainerFile } from '@/api/modules/container';
|
||||
import { batchCheckFiles, stopChunkUpload, uploadFileData } from '@/api/modules/files';
|
||||
import { CHUNK_SIZE, uploadChunkWithRetry } from '@/utils/chunk-upload';
|
||||
import i18n from '@/lang';
|
||||
@@ -134,6 +135,7 @@ import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
interface UploadFileProps {
|
||||
path: string;
|
||||
isAppendOnly?: boolean;
|
||||
containerID?: string;
|
||||
}
|
||||
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
@@ -154,6 +156,8 @@ const uploadOverallPercent = computed(() => {
|
||||
const open = ref(false);
|
||||
const isAppendOnly = ref(false);
|
||||
const path = ref();
|
||||
const containerID = ref('');
|
||||
const targetNode = ref('');
|
||||
let uploadHelper = ref('');
|
||||
const dialogExistFileRef = ref();
|
||||
const abortController = ref<AbortController | null>(null);
|
||||
@@ -213,9 +217,6 @@ const closePage = () => {
|
||||
clearFiles();
|
||||
em('close', false);
|
||||
};
|
||||
const state = reactive({
|
||||
uploadEle: null,
|
||||
});
|
||||
const uploaderFiles = ref<UploadFiles>([]);
|
||||
const hoverIndex = ref<number | null>(null);
|
||||
const tmpFiles = ref<UploadFiles>([]);
|
||||
@@ -225,9 +226,11 @@ const upload = (command: string) => {
|
||||
if (uploadLocked.value) {
|
||||
return;
|
||||
}
|
||||
state.uploadEle.webkitdirectory = command == 'dir';
|
||||
uploadRef.value.$el.querySelector('input').value = '';
|
||||
uploadRef.value.$el.querySelector('input').click();
|
||||
const input = uploadRef.value?.$el.querySelector('input[type=file]') as HTMLInputElement | null;
|
||||
if (!input) return;
|
||||
input.webkitdirectory = command === 'dir';
|
||||
input.value = '';
|
||||
input.click();
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
@@ -267,7 +270,7 @@ const handleDrop = async (event: DragEvent) => {
|
||||
};
|
||||
|
||||
const convertFileToUploadFile = (file: File, path: string): UploadFile => {
|
||||
const uid = Date.now();
|
||||
const uid = genFileId();
|
||||
|
||||
const uploadRawFile: UploadRawFile = new File([file], file.name, {
|
||||
type: file.type,
|
||||
@@ -294,12 +297,15 @@ const traverseFileTree = async (item: any, path = '') => {
|
||||
return;
|
||||
}
|
||||
if (item.isFile) {
|
||||
if (tmpFiles.value.length > 1000) {
|
||||
if (uploaderFiles.value.length + tmpFiles.value.length >= 1000) {
|
||||
breakFlag.value = true;
|
||||
return;
|
||||
}
|
||||
await new Promise<void>((resolve) => {
|
||||
item.file((file: File) => {
|
||||
if (uploaderFiles.value.length + tmpFiles.value.length >= 1000) {
|
||||
breakFlag.value = true;
|
||||
}
|
||||
if (!breakFlag.value) {
|
||||
tmpFiles.value.push(convertFileToUploadFile(file, path));
|
||||
}
|
||||
@@ -365,7 +371,14 @@ const handleSuccess: UploadProps['onSuccess'] = (res, file) => {
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
const files = uploaderFiles.value.slice();
|
||||
if (uploadLocked.value) return;
|
||||
const files = containerID.value
|
||||
? uploaderFiles.value.filter((file) => file.status !== 'success')
|
||||
: uploaderFiles.value.slice();
|
||||
if (containerID.value) {
|
||||
await uploadFile(files, false);
|
||||
return;
|
||||
}
|
||||
const fileNamesWithPath = Array.from(
|
||||
new Set(files.map((file) => `${path.value}/${file.raw.webkitRelativePath || file.name}`)),
|
||||
);
|
||||
@@ -410,7 +423,8 @@ const uploadFile = async (files: any[], overwrite: boolean) => {
|
||||
upLoading.value = true;
|
||||
uploadTotalCount.value = files.length;
|
||||
const controller = new AbortController();
|
||||
const uploadNode = currentNode.value;
|
||||
const uploadNode = containerID.value ? targetNode.value : currentNode.value;
|
||||
const uploadContainerID = containerID.value;
|
||||
abortController.value = controller;
|
||||
let successCount = 0;
|
||||
try {
|
||||
@@ -426,10 +440,27 @@ const uploadFile = async (files: any[], overwrite: boolean) => {
|
||||
|
||||
let isSuccess = false;
|
||||
try {
|
||||
isSuccess =
|
||||
Number(file.size) === 0
|
||||
? await uploadEmptyFile(file, controller, uploadNode, overwrite)
|
||||
: await uploadChunkedFile(file, controller, uploadNode, overwrite);
|
||||
if (uploadContainerID) {
|
||||
const data = new FormData();
|
||||
data.append('containerID', uploadContainerID);
|
||||
data.append('path', getUploadPath(file));
|
||||
data.append('file', file.raw, getFilenameFromPath(file.raw.webkitRelativePath || file.name));
|
||||
await uploadContainerFile(data, {
|
||||
headers: { CurrentNode: uploadNode },
|
||||
signal: controller.signal,
|
||||
skipErrorMessage: true,
|
||||
timeout: 0,
|
||||
onUploadProgress: ({ loaded, total }) => {
|
||||
uploadPercent.value = total ? Math.round((loaded / total) * 100) : 0;
|
||||
},
|
||||
});
|
||||
isSuccess = true;
|
||||
} else {
|
||||
isSuccess =
|
||||
Number(file.size) === 0
|
||||
? await uploadEmptyFile(file, controller, uploadNode, overwrite)
|
||||
: await uploadChunkedFile(file, controller, uploadNode, overwrite);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!controller.signal.aborted) {
|
||||
MsgError(getErrorMessage(error));
|
||||
@@ -573,6 +604,9 @@ const getFilenameFromPath = (path: string) => {
|
||||
};
|
||||
|
||||
const acceptParams = (props: UploadFileProps) => {
|
||||
if (uploadLocked.value) return;
|
||||
containerID.value = props.containerID || '';
|
||||
targetNode.value = currentNode.value;
|
||||
path.value = props.path;
|
||||
isAppendOnly.value = Boolean(props.isAppendOnly);
|
||||
open.value = true;
|
||||
@@ -581,22 +615,19 @@ const acceptParams = (props: UploadFileProps) => {
|
||||
uploadCurrentIndex.value = 0;
|
||||
uploadHelper.value = '';
|
||||
canceling.value = false;
|
||||
|
||||
nextTick(() => {
|
||||
state.uploadEle = document.querySelector('.el-upload__input');
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('beforeunload', (e) => {
|
||||
if (upLoading.value) {
|
||||
if (upLoading.value) {
|
||||
e.preventDefault();
|
||||
e.returnValue = i18n.global.t('file.cancelUploadHelper');
|
||||
return i18n.global.t('file.cancelUploadHelper');
|
||||
}
|
||||
}
|
||||
});
|
||||
const beforeUnload = (event: BeforeUnloadEvent) => {
|
||||
if (upLoading.value) {
|
||||
event.preventDefault();
|
||||
event.returnValue = i18n.global.t('file.cancelUploadHelper');
|
||||
}
|
||||
};
|
||||
onMounted(() => window.addEventListener('beforeunload', beforeUnload));
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('beforeunload', beforeUnload);
|
||||
abortController.value?.abort();
|
||||
void cleanupActiveChunkUpload();
|
||||
});
|
||||
|
||||
defineExpose({ acceptParams, handleDrop, open });
|
||||
|
||||
@@ -41,21 +41,14 @@
|
||||
<el-checkbox v-model="obtain.pushDir" :label="$t('ssl.pushDir')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('ssl.dir')" prop="dir" v-if="obtain.pushDir">
|
||||
<el-button
|
||||
icon="Folder"
|
||||
class="mb-2"
|
||||
@click="
|
||||
fileRef.acceptParams({
|
||||
path: obtain.dir.split('\n')[0],
|
||||
dir: true,
|
||||
multiple: true,
|
||||
selectedPaths: obtain.dir.split('\n').filter(Boolean),
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ $t('ssl.dir') }}
|
||||
</el-button>
|
||||
<el-input v-model.trim="obtain.dir" type="textarea" :rows="3" />
|
||||
<el-input v-model.trim="obtain.dir">
|
||||
<template #prepend>
|
||||
<el-button
|
||||
icon="Folder"
|
||||
@click="fileRef.acceptParams({ path: obtain.dir, dir: true })"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="input-help">
|
||||
{{ $t('ssl.pushDirHelper') }}
|
||||
</span>
|
||||
@@ -164,8 +157,8 @@ const resetForm = () => {
|
||||
obtain.value = initData();
|
||||
};
|
||||
|
||||
const getPath = (dirs: string[]) => {
|
||||
obtain.value.dir = dirs.join('\n');
|
||||
const getPath = (dir: string) => {
|
||||
obtain.value.dir = dir;
|
||||
};
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
|
||||
@@ -127,21 +127,11 @@
|
||||
<el-checkbox v-model="ssl.pushDir" :label="$t('ssl.pushDir')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('ssl.dir')" prop="dir" v-if="ssl.pushDir">
|
||||
<el-button
|
||||
icon="Folder"
|
||||
class="mb-2"
|
||||
@click="
|
||||
fileRef.acceptParams({
|
||||
path: ssl.dir.split('\n')[0],
|
||||
dir: true,
|
||||
multiple: true,
|
||||
selectedPaths: ssl.dir.split('\n').filter(Boolean),
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ $t('ssl.dir') }}
|
||||
</el-button>
|
||||
<el-input v-model.trim="ssl.dir" type="textarea" :rows="3" />
|
||||
<el-input v-model.trim="ssl.dir">
|
||||
<template #prepend>
|
||||
<el-button icon="Folder" @click="fileRef.acceptParams({ path: ssl.dir, dir: true })" />
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="input-help">
|
||||
{{ $t('ssl.pushDirHelper') }}
|
||||
</span>
|
||||
@@ -357,8 +347,8 @@ const acceptParams = (op: string, websiteSSL?: Website.SSLDTO) => {
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const getPath = (dirs: string[]) => {
|
||||
ssl.value.dir = dirs.join('\n');
|
||||
const getPath = (dir: string) => {
|
||||
ssl.value.dir = dir;
|
||||
};
|
||||
|
||||
const getAcmeAccounts = async () => {
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{{ ssl.acmeAccount.email }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('ssl.pushDir')" v-if="ssl.pushDir">
|
||||
<div v-for="dir in ssl.dir.split('\n')" :key="dir">{{ dir }}</div>
|
||||
{{ ssl.dir }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user