mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-27 17:26:03 +08:00
feat: Check for duplicate files before uploading/copying/moving (#8177)
This commit is contained in:
parent
e7124b26df
commit
47d5e919a2
25 changed files with 602 additions and 248 deletions
|
|
@ -417,6 +417,23 @@ func (b *BaseApi) CheckFile(c *gin.Context) {
|
|||
helper.SuccessWithData(c, true)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Batch check file exist
|
||||
// @Accept json
|
||||
// @Param request body request.FilePathsCheck true "request"
|
||||
// @Success 200 {array} response.ExistFileInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/batch/check [post]
|
||||
func (b *BaseApi) BatchCheckFiles(c *gin.Context) {
|
||||
var req request.FilePathsCheck
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
fileList := fileService.BatchCheckFiles(req)
|
||||
helper.SuccessWithData(c, fileList)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Change file name
|
||||
// @Accept json
|
||||
|
|
|
|||
|
|
@ -79,6 +79,10 @@ type FilePathCheck struct {
|
|||
Path string `json:"path" validate:"required"`
|
||||
}
|
||||
|
||||
type FilePathsCheck struct {
|
||||
Paths []string `json:"paths" validate:"required"`
|
||||
}
|
||||
|
||||
type FileWget struct {
|
||||
Url string `json:"url" validate:"required"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package response
|
|||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FileInfo struct {
|
||||
|
|
@ -46,3 +47,10 @@ type FileLineContent struct {
|
|||
type FileExist struct {
|
||||
Exist bool `json:"exist"`
|
||||
}
|
||||
|
||||
type ExistFileInfo struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime time.Time `json:"modTime"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ type IFileService interface {
|
|||
ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error)
|
||||
|
||||
GetPathByType(pathType string) string
|
||||
BatchCheckFiles(req request.FilePathsCheck) []response.ExistFileInfo
|
||||
}
|
||||
|
||||
var filteredPaths = []string{
|
||||
|
|
@ -518,3 +519,18 @@ func (f *FileService) GetPathByType(pathType string) string {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.ExistFileInfo {
|
||||
fileList := make([]response.ExistFileInfo, 0, len(req.Paths))
|
||||
for _, filePath := range req.Paths {
|
||||
if info, err := os.Stat(filePath); err == nil {
|
||||
fileList = append(fileList, response.ExistFileInfo{
|
||||
Size: info.Size(),
|
||||
Name: info.Name(),
|
||||
Path: filePath,
|
||||
ModTime: info.ModTime(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return fileList
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
|
|||
fileRouter.POST("/content", baseApi.GetContent)
|
||||
fileRouter.POST("/save", baseApi.SaveContent)
|
||||
fileRouter.POST("/check", baseApi.CheckFile)
|
||||
fileRouter.POST("/batch/check", baseApi.BatchCheckFiles)
|
||||
fileRouter.POST("/upload", baseApi.UploadFiles)
|
||||
fileRouter.POST("/chunkupload", baseApi.UploadChunkFiles)
|
||||
fileRouter.POST("/rename", baseApi.ChangeFileName)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ type Login struct {
|
|||
Captcha string `json:"captcha"`
|
||||
CaptchaID string `json:"captchaID"`
|
||||
AuthMethod string `json:"authMethod" validate:"required,oneof=jwt session"`
|
||||
Language string `json:"language" validate:"required,oneof=zh en tw ja ru ms 'pt-BR'"`
|
||||
Language string `json:"language" validate:"required,oneof=zh en 'zh-Hant' ko ja ru ms 'pt-BR'"`
|
||||
}
|
||||
|
||||
type MFALogin struct {
|
||||
|
|
|
|||
|
|
@ -152,6 +152,14 @@ export namespace File {
|
|||
path: string;
|
||||
}
|
||||
|
||||
export interface ExistFileInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
size: number;
|
||||
uploadSize: number;
|
||||
modTime: string;
|
||||
}
|
||||
|
||||
export interface RecycleBin {
|
||||
sourcePath: string;
|
||||
name: string;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ export const uploadFileData = (params: FormData, config: AxiosRequestConfig) =>
|
|||
return http.upload<File.File>('files/upload', params, config);
|
||||
};
|
||||
|
||||
export const batchCheckFiles = (paths: string[]) => {
|
||||
return http.post<File.ExistFileInfo[]>('files/batch/check', { paths: paths }, TimeoutEnum.T_5M);
|
||||
};
|
||||
|
||||
export const chunkUploadFileData = (params: FormData, config: AxiosRequestConfig) => {
|
||||
return http.upload<File.File>('files/chunkupload', params, config);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ const size = computed(() => {
|
|||
return '100%';
|
||||
case '60%':
|
||||
return '60%';
|
||||
case props.size:
|
||||
return props.size;
|
||||
default:
|
||||
return '50%';
|
||||
}
|
||||
|
|
|
|||
78
frontend/src/components/exist-file/index.vue
Normal file
78
frontend/src/components/exist-file/index.vue
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="$t('file.existFileTitle')"
|
||||
width="35%"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<el-alert :show-icon="true" type="warning" :closable="false">
|
||||
<div class="whitespace-break-spaces">
|
||||
<span>{{ $t('file.existFileHelper') }}</span>
|
||||
</div>
|
||||
</el-alert>
|
||||
<div>
|
||||
<el-table :data="existFiles" max-height="350">
|
||||
<el-table-column type="index" :label="$t('commons.table.serialNumber')" width="55" />
|
||||
<el-table-column prop="path" :label="$t('commons.table.name')" :min-width="200" />
|
||||
<el-table-column :label="$t('file.existFileSize')" width="230">
|
||||
<template #default="{ row }">
|
||||
{{ getFileSize(row.uploadSize) }} -> {{ getFileSize(row.size) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleSkip">{{ $t('commons.button.skip') }}</el-button>
|
||||
<el-button type="primary" @click="handleOverwrite()">
|
||||
{{ $t('commons.button.cover') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { computeSize } from '@/utils/util';
|
||||
|
||||
const dialogVisible = ref();
|
||||
const existFiles = ref<DialogProps[]>([]);
|
||||
|
||||
interface DialogProps {
|
||||
name: string;
|
||||
path: string;
|
||||
size: number;
|
||||
uploadSize: number;
|
||||
modTime: string;
|
||||
}
|
||||
let onConfirmCallback = null;
|
||||
const getFileSize = (size: number) => {
|
||||
return computeSize(size);
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
dialogVisible.value = false;
|
||||
if (onConfirmCallback) {
|
||||
onConfirmCallback(
|
||||
'skip',
|
||||
existFiles.value.map((file) => file.path),
|
||||
);
|
||||
}
|
||||
};
|
||||
const handleOverwrite = () => {
|
||||
dialogVisible.value = false;
|
||||
if (onConfirmCallback) {
|
||||
onConfirmCallback('overwrite');
|
||||
}
|
||||
};
|
||||
const acceptParams = async ({ paths, onConfirm }): Promise<void> => {
|
||||
existFiles.value = paths;
|
||||
onConfirmCallback = onConfirm;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
defineExpose({ acceptParams });
|
||||
</script>
|
||||
|
|
@ -73,6 +73,8 @@ const message = {
|
|||
helpDoc: 'Help Document',
|
||||
bind: 'Bind',
|
||||
unbind: 'Unbind',
|
||||
cover: 'cover',
|
||||
skip: 'skip',
|
||||
fix: 'Fix',
|
||||
down: 'Stop',
|
||||
up: 'Start',
|
||||
|
|
@ -126,6 +128,7 @@ const message = {
|
|||
refreshRateUnit: '{0} Seconds/Time',
|
||||
selectColumn: 'Select column',
|
||||
local: 'local',
|
||||
serialNumber: 'Serial number',
|
||||
},
|
||||
loadingText: {
|
||||
Upgrading: 'System upgrade, please wait...',
|
||||
|
|
@ -1411,6 +1414,10 @@ const message = {
|
|||
fileCanNotRead: 'File can not read',
|
||||
panelInstallDir: '1Panel installation directory cannot be deleted',
|
||||
wgetTask: 'Download Task',
|
||||
existFileTitle: 'Same name file prompt',
|
||||
existFileHelper: 'The uploaded file contains a file with the same name, do you want to overwrite it?',
|
||||
existFileSize: 'File size (new -> old)',
|
||||
existFileDirHelper: 'The selected file/folder has a duplicate name. Please proceed with caution!',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: 'Auto Start',
|
||||
|
|
|
|||
|
|
@ -52,13 +52,13 @@ const message = {
|
|||
get: '得る',
|
||||
upgrade: 'アップグレード',
|
||||
update: '編集',
|
||||
ignore: 'アップグレードを無視します',
|
||||
ignore: '更新を無視する',
|
||||
copy: 'コピー',
|
||||
random: 'ランダム',
|
||||
install: 'インストール',
|
||||
uninstall: 'アンインストール',
|
||||
fullscreen: 'フルスクリーンを入力します',
|
||||
quitFullscreen: 'フルスクリーンを終了します',
|
||||
fullscreen: 'フルスクリーン',
|
||||
quitFullscreen: 'フルスクリーンを終了',
|
||||
showAll: 'すべてを表示します',
|
||||
hideSome: 'いくつかを隠します',
|
||||
agree: '同意する',
|
||||
|
|
@ -70,6 +70,8 @@ const message = {
|
|||
createNewFile: '新しいファイルを作成します',
|
||||
helpDoc: '文書をヘルプします',
|
||||
unbind: 'バインド',
|
||||
cover: 'に覆いを',
|
||||
skip: 'スキップ',
|
||||
fix: '修正',
|
||||
down: '停止',
|
||||
up: '起動',
|
||||
|
|
@ -115,9 +117,10 @@ const message = {
|
|||
protocol: 'プロトコル',
|
||||
tableSetting: 'テーブル設定',
|
||||
refreshRate: 'リフレッシュレート',
|
||||
refreshRateUnit: '更新なし|{n}秒/時間 |{n}秒/時間',
|
||||
refreshRateUnit: '更新なし|{n}秒/時 |{n}秒/時',
|
||||
selectColumn: '列を選択します',
|
||||
local: 'ローカル',
|
||||
serialNumber: 'シリアル番号',
|
||||
},
|
||||
loadingText: {
|
||||
Upgrading: 'システムのアップグレード、待ってください...',
|
||||
|
|
@ -1350,6 +1353,10 @@ const message = {
|
|||
fileCanNotRead: 'ファイルは読み取れません',
|
||||
panelInstallDir: `1Panelインストールディレクトリは削除できません`,
|
||||
wgetTask: 'ダウンロードタスク',
|
||||
existFileTitle: '同名ファイルの警告',
|
||||
existFileHelper: 'アップロードしたファイルに同じ名前のファイルが含まれています。上書きしますか?',
|
||||
existFileSize: 'ファイルサイズ(新しい -> 古い)',
|
||||
existFileDirHelper: '選択したファイル/フォルダーには同じ名前のものが既に存在します。慎重に操作してください!',
|
||||
},
|
||||
ssh: {
|
||||
setting: '設定',
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ const message = {
|
|||
createNewFile: '새 파일 생성',
|
||||
helpDoc: '도움말 문서',
|
||||
unbind: '연결 해제',
|
||||
cover: '덮어쓰기',
|
||||
skip: '건너뛰기',
|
||||
fix: '수정',
|
||||
down: '중지',
|
||||
up: '시작',
|
||||
|
|
@ -118,6 +120,7 @@ const message = {
|
|||
refreshRateUnit: '새로 고침 안 함 | {n} 초/회 | {n} 초/회',
|
||||
selectColumn: '열 선택',
|
||||
local: '로컬',
|
||||
serialNumber: '일련 번호',
|
||||
},
|
||||
loadingText: {
|
||||
Upgrading: '시스템 업그레이드 중입니다. 잠시만 기다려 주십시오...',
|
||||
|
|
@ -1337,6 +1340,10 @@ const message = {
|
|||
fileCanNotRead: '파일을 읽을 수 없습니다.',
|
||||
panelInstallDir: `1Panel 설치 디렉터리는 삭제할 수 없습니다.`,
|
||||
wgetTask: '다운로드 작업',
|
||||
existFileTitle: '동일한 이름의 파일 경고',
|
||||
existFileHelper: '업로드한 파일에 동일한 이름의 파일이 포함되어 있습니다. 덮어쓰시겠습니까?',
|
||||
existFileSize: '파일 크기 (새로운 -> 오래된)',
|
||||
existFileDirHelper: '선택한 파일/폴더에 동일한 이름이 이미 존재합니다. 신중하게 작업하세요!',
|
||||
},
|
||||
ssh: {
|
||||
setting: '설정',
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ const message = {
|
|||
createNewFile: 'Cipta fail baru',
|
||||
helpDoc: 'Dokumen Bantuan',
|
||||
unbind: 'Nyahkaitkan',
|
||||
cover: 'Tindih',
|
||||
skip: 'Langkau',
|
||||
fix: 'Betulkan',
|
||||
down: 'Hentikan',
|
||||
up: 'Mulakan',
|
||||
|
|
@ -118,6 +120,7 @@ const message = {
|
|||
refreshRateUnit: 'Tiada penyegaran | {n} saat/masa | {n} saat/masa',
|
||||
selectColumn: 'Pilih lajur',
|
||||
local: 'Tempatan',
|
||||
serialNumber: 'Nombor siri',
|
||||
},
|
||||
loadingText: {
|
||||
Upgrading: 'Peningkatan sistem, sila tunggu...',
|
||||
|
|
@ -1394,6 +1397,10 @@ const message = {
|
|||
fileCanNotRead: 'Fail tidak dapat dibaca',
|
||||
panelInstallDir: 'Direktori pemasangan 1Panel tidak boleh dipadamkan',
|
||||
wgetTask: 'Tugas Muat Turun',
|
||||
existFileTitle: 'Amaran fail dengan nama yang sama',
|
||||
existFileHelper: 'Fail yang dimuat naik mengandungi fail dengan nama yang sama. Adakah anda mahu menimpanya?',
|
||||
existFileSize: 'Saiz fail (baru -> lama)',
|
||||
existFileDirHelper: 'Fail/folder yang dipilih mempunyai nama yang sama. Sila berhati-hati!',
|
||||
},
|
||||
ssh: {
|
||||
setting: 'tetapan',
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ const message = {
|
|||
createNewFile: 'Criar novo arquivo',
|
||||
helpDoc: 'Documento de ajuda',
|
||||
unbind: 'Desvincular',
|
||||
cover: 'Substituir',
|
||||
skip: 'Pular',
|
||||
fix: 'Corrigir',
|
||||
down: 'Parar',
|
||||
up: 'Iniciar',
|
||||
|
|
@ -118,6 +120,7 @@ const message = {
|
|||
refreshRateUnit: 'Sem atualização | {n} segundo/atualização | {n} segundos/atualização',
|
||||
selectColumn: 'Selecionar coluna',
|
||||
local: 'Local',
|
||||
serialNumber: 'Número de série',
|
||||
},
|
||||
loadingText: {
|
||||
Upgrading: 'Atualizando o sistema, por favor, aguarde...',
|
||||
|
|
@ -1379,6 +1382,10 @@ const message = {
|
|||
fileCanNotRead: 'O arquivo não pode ser lido',
|
||||
panelInstallDir: 'O diretório de instalação do 1Panel não pode ser excluído',
|
||||
wgetTask: 'Tarefa de Download',
|
||||
existFileTitle: 'Aviso de arquivo com o mesmo nome',
|
||||
existFileHelper: 'O arquivo enviado contém um arquivo com o mesmo nome. Deseja substituí-lo?',
|
||||
existFileSize: 'Tamanho do arquivo (novo -> antigo)',
|
||||
existFileDirHelper: 'O arquivo/pasta selecionado tem um nome duplicado. Por favor, prossiga com cautela!',
|
||||
},
|
||||
ssh: {
|
||||
setting: 'configuração',
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const message = {
|
|||
collapse: 'Свернуть',
|
||||
log: 'Логи',
|
||||
back: 'Назад',
|
||||
backup: 'Резервное копирование',
|
||||
backup: 'Бэкап',
|
||||
recover: 'Восстановить',
|
||||
retry: 'Повторить',
|
||||
upload: 'Загрузить',
|
||||
|
|
@ -70,6 +70,8 @@ const message = {
|
|||
createNewFile: 'Создать новый файл',
|
||||
helpDoc: 'Справка',
|
||||
unbind: 'Отвязать',
|
||||
cover: 'Заменить',
|
||||
skip: 'Пропустить',
|
||||
fix: 'Исправить',
|
||||
down: 'Остановить',
|
||||
up: 'Запустить',
|
||||
|
|
@ -118,6 +120,7 @@ const message = {
|
|||
refreshRateUnit: 'Без обновления | {n} секунда/раз | {n} секунд/раз',
|
||||
selectColumn: 'Выбрать столбец',
|
||||
local: 'локальный',
|
||||
serialNumber: 'Серийный номер',
|
||||
},
|
||||
loadingText: {
|
||||
Upgrading: 'Обновление системы, пожалуйста, подождите...',
|
||||
|
|
@ -1384,6 +1387,10 @@ const message = {
|
|||
fileCanNotRead: 'Файл не может быть прочитан',
|
||||
panelInstallDir: 'Директорию установки 1Panel нельзя удалить',
|
||||
wgetTask: 'Задача загрузки',
|
||||
existFileTitle: 'Предупреждение о файле с тем же именем',
|
||||
existFileHelper: 'Загруженный файл содержит файл с таким же именем. Заменить его?',
|
||||
existFileSize: 'Размер файла (новый -> старый)',
|
||||
existFileDirHelper: 'Выбранный файл/папка имеет дублирующееся имя. Пожалуйста, действуйте осторожно!',
|
||||
},
|
||||
ssh: {
|
||||
setting: 'настройка',
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ const message = {
|
|||
helpDoc: '幫助文档',
|
||||
bind: '綁定',
|
||||
unbind: '解綁',
|
||||
cover: '覆蓋',
|
||||
skip: '跳過',
|
||||
fix: '修復',
|
||||
down: '停止',
|
||||
up: '啟動',
|
||||
|
|
@ -123,6 +125,7 @@ const message = {
|
|||
refreshRateUnit: '{0} 秒/次',
|
||||
selectColumn: '選擇列',
|
||||
local: '本地',
|
||||
serialNumber: '序號',
|
||||
},
|
||||
loadingText: {
|
||||
Upgrading: '系統升級中,請稍候...',
|
||||
|
|
@ -1338,6 +1341,10 @@ const message = {
|
|||
fileCanNotRead: '此文件不支持預覽',
|
||||
panelInstallDir: '1Panel 安裝目錄不能删除',
|
||||
wgetTask: '下載任務',
|
||||
existFileTitle: '同名檔案提示',
|
||||
existFileHelper: '上傳的檔案存在同名檔案,是否覆蓋?',
|
||||
existFileSize: '文件大小(新->舊)',
|
||||
existFileDirHelper: '選擇的檔案/資料夾存在同名,請謹慎操作!',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: '開機自啟',
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ const message = {
|
|||
helpDoc: '帮助文档',
|
||||
bind: '绑定',
|
||||
unbind: '解绑',
|
||||
cover: '覆盖',
|
||||
skip: '跳过',
|
||||
fix: '修复',
|
||||
down: '停止',
|
||||
up: '启动',
|
||||
|
|
@ -123,6 +125,7 @@ const message = {
|
|||
refreshRateUnit: '{0} 秒/次',
|
||||
selectColumn: '选择列',
|
||||
local: '本地',
|
||||
serialNumber: '序号',
|
||||
},
|
||||
loadingText: {
|
||||
Upgrading: '系统升级中,请稍候...',
|
||||
|
|
@ -1334,6 +1337,10 @@ const message = {
|
|||
fileCanNotRead: '此文件不支持预览',
|
||||
panelInstallDir: '1Panel 安装目录不能删除',
|
||||
wgetTask: '下载任务',
|
||||
existFileTitle: '同名文件提示',
|
||||
existFileHelper: '上传的文件存在同名文件,是否覆盖?',
|
||||
existFileSize: '文件大小 (新 -> 旧)',
|
||||
existFileDirHelper: '选择的文件/文件夹存在同名,请谨慎操作!',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: '开机自启',
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
:collapse-transition="false"
|
||||
:unique-opened="true"
|
||||
@select="handleMenuClick"
|
||||
class="custom-menu"
|
||||
>
|
||||
<SubItem :menuList="routerMenus" />
|
||||
<el-menu-item :index="''" @click="logout">
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@
|
|||
}
|
||||
|
||||
.a-detail {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.d-name {
|
||||
height: 20%;
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
@ -26,22 +22,15 @@
|
|||
}
|
||||
.h-button {
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.msg {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.el-button + .el-button {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.d-description {
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
.el-tag {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
|
@ -53,7 +42,7 @@
|
|||
}
|
||||
.d-button {
|
||||
margin-top: 10px;
|
||||
min-width: 440px;
|
||||
min-width: 330px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +55,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.table-button {
|
||||
display: inline;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.app-divider {
|
||||
margin-top: 5px;
|
||||
border: 0;
|
||||
|
|
@ -87,7 +81,6 @@
|
|||
}
|
||||
|
||||
.tag-button {
|
||||
margin-right: 10px;
|
||||
&.no-active {
|
||||
background: none;
|
||||
border: none;
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@
|
|||
:md="24"
|
||||
:lg="12"
|
||||
:xl="12"
|
||||
:class="mode === 'upgrade' ? 'upgrade-card-col-12' : 'install-card-col-12'"
|
||||
>
|
||||
<div class="install-card">
|
||||
<el-card class="e-card">
|
||||
|
|
@ -113,122 +112,128 @@
|
|||
<el-col :xs="24" :sm="21" :md="21" :lg="20" :xl="20">
|
||||
<div class="a-detail">
|
||||
<div class="d-name">
|
||||
<el-button link type="info">
|
||||
<el-tooltip :content="installed.name" placement="top">
|
||||
<span class="name">{{ installed.name }}</span>
|
||||
</el-tooltip>
|
||||
</el-button>
|
||||
<span class="status">
|
||||
<Status
|
||||
:key="installed.status"
|
||||
:status="installed.status"
|
||||
></Status>
|
||||
</span>
|
||||
<span class="msg">
|
||||
<el-popover
|
||||
v-if="isAppErr(installed)"
|
||||
placement="bottom"
|
||||
:width="400"
|
||||
trigger="hover"
|
||||
:content="installed.message"
|
||||
:popper-options="options"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button link type="danger">
|
||||
<el-icon><Warning /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="app-error">
|
||||
{{ installed.message }}
|
||||
</div>
|
||||
</el-popover>
|
||||
</span>
|
||||
<span class="ml-1">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('app.toFolder')"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="toFolder(installed.path)"
|
||||
>
|
||||
<el-icon>
|
||||
<FolderOpened />
|
||||
</el-icon>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="min-w-50 flex items-center justify-start gap-1">
|
||||
<el-button link type="info">
|
||||
<el-tooltip :content="installed.name" placement="top">
|
||||
<span class="name">{{ installed.name }}</span>
|
||||
</el-tooltip>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span class="ml-1">
|
||||
<el-tooltip
|
||||
v-if="mode !== 'upgrade'"
|
||||
effect="dark"
|
||||
:content="$t('commons.button.log')"
|
||||
placement="top"
|
||||
>
|
||||
<span class="status">
|
||||
<Status
|
||||
:key="installed.status"
|
||||
:status="installed.status"
|
||||
></Status>
|
||||
</span>
|
||||
<span class="msg">
|
||||
<el-popover
|
||||
v-if="isAppErr(installed)"
|
||||
placement="bottom"
|
||||
:width="400"
|
||||
trigger="hover"
|
||||
:content="installed.message"
|
||||
:popper-options="options"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button link type="danger">
|
||||
<el-icon><Warning /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="app-error">
|
||||
{{ installed.message }}
|
||||
</div>
|
||||
</el-popover>
|
||||
</span>
|
||||
<span class="ml-1">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('app.toFolder')"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="toFolder(installed.path)"
|
||||
>
|
||||
<el-icon>
|
||||
<FolderOpened />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span class="ml-1">
|
||||
<el-tooltip
|
||||
v-if="mode !== 'upgrade'"
|
||||
effect="dark"
|
||||
:content="$t('commons.button.log')"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="openLog(installed)"
|
||||
:disabled="installed.status === 'DownloadErr'"
|
||||
>
|
||||
<el-icon><Tickets /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-end gap-1">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="openLog(installed)"
|
||||
:disabled="installed.status === 'DownloadErr'"
|
||||
class="h-button"
|
||||
plain
|
||||
round
|
||||
size="small"
|
||||
@click="openUploads(installed.appKey, installed.name)"
|
||||
v-if="mode === 'installed'"
|
||||
>
|
||||
<el-icon><Tickets /></el-icon>
|
||||
{{ $t('database.loadBackup') }}
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-button
|
||||
class="h-button"
|
||||
plain
|
||||
round
|
||||
size="small"
|
||||
@click="openUploads(installed.appKey, installed.name)"
|
||||
v-if="mode === 'installed'"
|
||||
>
|
||||
{{ $t('database.loadBackup') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="h-button"
|
||||
plain
|
||||
round
|
||||
size="small"
|
||||
@click="
|
||||
openBackups(
|
||||
installed.appKey,
|
||||
installed.name,
|
||||
installed.status,
|
||||
)
|
||||
"
|
||||
v-if="mode === 'installed'"
|
||||
>
|
||||
{{ $t('commons.button.backup') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="h-button"
|
||||
plain
|
||||
round
|
||||
size="small"
|
||||
:disabled="installed.status === 'Upgrading'"
|
||||
@click="openOperate(installed, 'ignore')"
|
||||
v-if="mode === 'upgrade'"
|
||||
>
|
||||
{{ $t('commons.button.ignore') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="h-button"
|
||||
plain
|
||||
round
|
||||
size="small"
|
||||
:disabled="
|
||||
(installed.status !== 'Running' &&
|
||||
installed.status !== 'UpgradeErr') ||
|
||||
installed.appStatus === 'TakeDown'
|
||||
"
|
||||
@click="openOperate(installed, 'upgrade')"
|
||||
v-if="mode === 'upgrade'"
|
||||
>
|
||||
{{ $t('commons.button.upgrade') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="h-button"
|
||||
plain
|
||||
round
|
||||
size="small"
|
||||
@click="
|
||||
openBackups(
|
||||
installed.appKey,
|
||||
installed.name,
|
||||
installed.status,
|
||||
)
|
||||
"
|
||||
v-if="mode === 'installed'"
|
||||
>
|
||||
{{ $t('commons.button.backup') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="h-button"
|
||||
plain
|
||||
round
|
||||
size="small"
|
||||
:disabled="installed.status === 'Upgrading'"
|
||||
@click="openOperate(installed, 'ignore')"
|
||||
v-if="mode === 'upgrade'"
|
||||
>
|
||||
{{ $t('commons.button.ignore') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="h-button"
|
||||
plain
|
||||
round
|
||||
size="small"
|
||||
:disabled="
|
||||
(installed.status !== 'Running' &&
|
||||
installed.status !== 'UpgradeErr') ||
|
||||
installed.appStatus === 'TakeDown'
|
||||
"
|
||||
@click="openOperate(installed, 'upgrade')"
|
||||
v-if="mode === 'upgrade'"
|
||||
>
|
||||
{{ $t('commons.button.upgrade') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="d-description flex flex-wrap items-center justify-start gap-1.5"
|
||||
|
|
@ -295,13 +300,12 @@
|
|||
{{ $t('app.webUIConfig') }}
|
||||
</span>
|
||||
</el-popover>
|
||||
|
||||
<div class="description">
|
||||
<span>
|
||||
{{ $t('app.alreadyRun') }}{{ $t('commons.colon') }}
|
||||
{{ getAge(installed.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
<span>
|
||||
{{ $t('app.alreadyRun') }}{{ $t('commons.colon') }}
|
||||
{{ getAge(installed.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="app-divider" />
|
||||
<div
|
||||
|
|
@ -729,38 +733,16 @@ onUnmounted(() => {
|
|||
|
||||
<style scoped lang="scss">
|
||||
@use '../index';
|
||||
@media only screen and (max-width: 1400px) {
|
||||
.install-card-col-12 {
|
||||
max-width: 100%;
|
||||
flex: 0 0 100%;
|
||||
.a-detail {
|
||||
.d-name {
|
||||
.name {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1499px) {
|
||||
.upgrade-card-col-12 {
|
||||
max-width: 100%;
|
||||
flex: 0 0 100%;
|
||||
.a-detail {
|
||||
.d-name {
|
||||
.name {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-error {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.d-name {
|
||||
.el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.d-button {
|
||||
.el-button + .el-button {
|
||||
margin-left: 0;
|
||||
|
|
|
|||
|
|
@ -391,7 +391,7 @@ const codeReq = reactive({ path: '', expand: false, page: 1, pageSize: 100 });
|
|||
const fileUpload = reactive({ path: '' });
|
||||
const fileRename = reactive({ path: '', oldName: '' });
|
||||
const fileWget = reactive({ path: '' });
|
||||
const fileMove = reactive({ oldPaths: [''], type: '', path: '', name: '', count: 0 });
|
||||
const fileMove = reactive({ oldPaths: [''], allNames: [''], type: '', path: '', name: '', count: 0, isDir: false });
|
||||
|
||||
const createRef = ref();
|
||||
const roleRef = ref();
|
||||
|
|
@ -788,14 +788,23 @@ const openRename = (item: File.File) => {
|
|||
const openMove = (type: string) => {
|
||||
fileMove.type = type;
|
||||
fileMove.name = '';
|
||||
const oldpaths = [];
|
||||
fileMove.allNames = [];
|
||||
fileMove.isDir = false;
|
||||
const oldPaths = [];
|
||||
for (const s of selects.value) {
|
||||
oldpaths.push(s['path']);
|
||||
oldPaths.push(s['path']);
|
||||
}
|
||||
fileMove.count = selects.value.length;
|
||||
fileMove.oldPaths = oldpaths;
|
||||
fileMove.oldPaths = oldPaths;
|
||||
if (selects.value.length == 1) {
|
||||
fileMove.name = selects.value[0].name;
|
||||
fileMove.isDir = selects.value[0].isDir;
|
||||
} else {
|
||||
const allNames = [];
|
||||
for (const s of selects.value) {
|
||||
allNames.push(s['name']);
|
||||
}
|
||||
fileMove.allNames = allNames;
|
||||
}
|
||||
moveOpen.value = true;
|
||||
};
|
||||
|
|
@ -806,6 +815,7 @@ const closeMove = () => {
|
|||
fileMove.oldPaths = [];
|
||||
fileMove.name = '';
|
||||
fileMove.count = 0;
|
||||
fileMove.isDir = false;
|
||||
moveOpen.value = false;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<DrawerPro v-model="open" :header="title" @close="handleClose" size="normal">
|
||||
<DrawerPro v-model="open" :header="title" @close="handleClose" size="675">
|
||||
<el-form
|
||||
@submit.prevent
|
||||
ref="fileForm"
|
||||
|
|
@ -22,6 +22,23 @@
|
|||
<el-radio :value="false" size="large">{{ $t('file.rename') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div v-if="existFiles.length > 0 && !changeName" class="text-center">
|
||||
<el-alert :show-icon="true" type="warning" :closable="false">
|
||||
<div class="whitespace-break-spaces">
|
||||
<span>{{ $t('file.existFileDirHelper') }}</span>
|
||||
</div>
|
||||
</el-alert>
|
||||
<el-transfer
|
||||
v-model="skipFiles"
|
||||
class="text-left inline-block mt-4"
|
||||
:titles="[$t('commons.button.cover'), $t('commons.button.skip')]"
|
||||
:format="{
|
||||
noChecked: '${total}',
|
||||
hasChecked: '${checked}/${total}',
|
||||
}"
|
||||
:data="transferData"
|
||||
/>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
|
|
@ -35,20 +52,22 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { checkFile, moveFile } from '@/api/modules/files';
|
||||
import { batchCheckFiles, checkFile, moveFile } from '@/api/modules/files';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { FormInstance, FormRules } from 'element-plus';
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { ref, reactive, computed, ComputedRef } from 'vue';
|
||||
import FileList from '@/components/file-list/index.vue';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { getDateStr } from '@/utils/util';
|
||||
|
||||
interface MoveProps {
|
||||
oldPaths: Array<string>;
|
||||
allNames: Array<string>;
|
||||
type: string;
|
||||
path: string;
|
||||
name: string;
|
||||
isDir: boolean;
|
||||
}
|
||||
|
||||
const fileForm = ref<FormInstance>();
|
||||
|
|
@ -57,6 +76,9 @@ const open = ref(false);
|
|||
const type = ref('cut');
|
||||
const changeName = ref(false);
|
||||
const oldName = ref('');
|
||||
const existFiles = ref([]);
|
||||
const skipFiles = ref([]);
|
||||
const transferData = ref([]);
|
||||
|
||||
const title = computed(() => {
|
||||
if (type.value === 'cut') {
|
||||
|
|
@ -71,6 +93,8 @@ const addForm = reactive({
|
|||
newPath: '',
|
||||
type: '',
|
||||
name: '',
|
||||
allNames: [] as string[],
|
||||
isDir: false,
|
||||
cover: false,
|
||||
});
|
||||
|
||||
|
|
@ -89,6 +113,18 @@ const handleClose = (search: boolean) => {
|
|||
em('close', search);
|
||||
};
|
||||
|
||||
const getFileName = (filePath: string) => {
|
||||
if (filePath.endsWith('/')) {
|
||||
filePath = filePath.slice(0, -1);
|
||||
}
|
||||
|
||||
return filePath.split('/').pop();
|
||||
};
|
||||
|
||||
const coverFiles: ComputedRef<string[]> = computed(() => {
|
||||
return addForm.oldPaths.filter((item) => !skipFiles.value.includes(getFileName(item))).map((item) => item);
|
||||
});
|
||||
|
||||
const getPath = (path: string) => {
|
||||
addForm.newPath = path;
|
||||
};
|
||||
|
|
@ -97,7 +133,7 @@ const changeType = () => {
|
|||
if (addForm.cover) {
|
||||
addForm.name = oldName.value;
|
||||
} else {
|
||||
addForm.name = oldName.value + '-' + getDateStr();
|
||||
addForm.name = renameFileWithSuffix(oldName.value, addForm.isDir);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -123,16 +159,64 @@ const submit = async (formEl: FormInstance | undefined) => {
|
|||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
addForm.oldPaths = coverFiles.value;
|
||||
mvFile();
|
||||
});
|
||||
};
|
||||
|
||||
const getCompleteExtension = (filename: string): string => {
|
||||
const compoundExtensions = [
|
||||
'.tar.gz',
|
||||
'.tar.bz2',
|
||||
'.tar.xz',
|
||||
'.tar.lzma',
|
||||
'.tar.Z',
|
||||
'.tar.zst',
|
||||
'.tar.lzo',
|
||||
'.tar.sz',
|
||||
'.tgz',
|
||||
'.tbz2',
|
||||
'.txz',
|
||||
'.tzst',
|
||||
];
|
||||
const foundExtension = compoundExtensions.find((ext) => filename.endsWith(ext));
|
||||
if (foundExtension) {
|
||||
return foundExtension;
|
||||
}
|
||||
const match = filename.match(/\.[a-zA-Z0-9]+$/);
|
||||
return match ? match[0] : '';
|
||||
};
|
||||
|
||||
const renameFileWithSuffix = (fileName: string, isDir: boolean): string => {
|
||||
const insertStr = '-' + getDateStr();
|
||||
const completeExt = isDir ? '' : getCompleteExtension(fileName);
|
||||
if (!completeExt) {
|
||||
return `${fileName}${insertStr}`;
|
||||
} else {
|
||||
const baseName = fileName.slice(0, fileName.length - completeExt.length);
|
||||
return `${baseName}${insertStr}${completeExt}`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilePaths = async (fileNames: string[], newPath: string) => {
|
||||
const uniqueFiles = [...new Set(fileNames)];
|
||||
const fileNamesWithPath = uniqueFiles.map((file) => newPath + '/' + file);
|
||||
const existData = await batchCheckFiles(fileNamesWithPath);
|
||||
existFiles.value = existData.data;
|
||||
transferData.value = existData.data.map((file) => ({
|
||||
key: file.name,
|
||||
label: file.name,
|
||||
}));
|
||||
};
|
||||
|
||||
const acceptParams = async (props: MoveProps) => {
|
||||
changeName.value = false;
|
||||
addForm.oldPaths = props.oldPaths;
|
||||
addForm.type = props.type;
|
||||
addForm.newPath = props.path;
|
||||
addForm.isDir = props.isDir;
|
||||
addForm.name = '';
|
||||
addForm.allNames = props.allNames;
|
||||
type.value = props.type;
|
||||
if (props.name && props.name != '') {
|
||||
oldName.value = props.name;
|
||||
|
|
@ -140,7 +224,15 @@ const acceptParams = async (props: MoveProps) => {
|
|||
if (res.data) {
|
||||
changeName.value = true;
|
||||
addForm.cover = false;
|
||||
addForm.name = props.name + '-' + getDateStr();
|
||||
addForm.name = renameFileWithSuffix(props.name, addForm.isDir);
|
||||
open.value = true;
|
||||
} else {
|
||||
mvFile();
|
||||
}
|
||||
} else if (props.allNames && props.allNames.length > 0) {
|
||||
await handleFilePaths(addForm.allNames, addForm.newPath);
|
||||
if (existFiles.value.length > 0) {
|
||||
changeName.value = false;
|
||||
open.value = true;
|
||||
} else {
|
||||
mvFile();
|
||||
|
|
@ -152,3 +244,41 @@ const acceptParams = async (props: MoveProps) => {
|
|||
|
||||
defineExpose({ acceptParams });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-transfer) {
|
||||
--el-transfer-panel-width: 250px;
|
||||
.el-button {
|
||||
padding: 4px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-transfer__buttons) {
|
||||
padding: 5px 15px;
|
||||
@media (max-width: 600px) {
|
||||
width: 250px;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
.el-button [class*='el-icon'] svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 601px) {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
justify-content: center;
|
||||
.el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-transfer-panel .el-transfer-panel__footer) {
|
||||
height: 65px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -91,16 +91,18 @@
|
|||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
<ExistFileDialog ref="dialogExistFileRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, reactive, ref } from 'vue';
|
||||
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
|
||||
import { chunkUploadFileData, uploadFileData } from '@/api/modules/files';
|
||||
import { batchCheckFiles, chunkUploadFileData, uploadFileData } from '@/api/modules/files';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message';
|
||||
import { Close, Document, UploadFilled } from '@element-plus/icons-vue';
|
||||
import { TimeoutEnum } from '@/enums/http-enum';
|
||||
import ExistFileDialog from '@/components/exist-file/index.vue';
|
||||
|
||||
interface UploadFileProps {
|
||||
path: string;
|
||||
|
|
@ -112,6 +114,7 @@ let uploadPercent = ref(0);
|
|||
const open = ref(false);
|
||||
const path = ref();
|
||||
let uploadHelper = ref('');
|
||||
const dialogExistFileRef = ref();
|
||||
|
||||
const em = defineEmits(['close']);
|
||||
const handleClose = () => {
|
||||
|
|
@ -126,6 +129,8 @@ const uploaderFiles = ref<UploadFiles>([]);
|
|||
const hoverIndex = ref<number | null>(null);
|
||||
const tmpFiles = ref<UploadFiles>([]);
|
||||
const breakFlag = ref(false);
|
||||
const CHUNK_SIZE = 1024 * 1024 * 5;
|
||||
const MAX_SINGLE_FILE_SIZE = 1024 * 1024 * 10;
|
||||
|
||||
const upload = (command: string) => {
|
||||
state.uploadEle.webkitdirectory = command == 'dir';
|
||||
|
|
@ -262,87 +267,125 @@ const handleSuccess: UploadProps['onSuccess'] = (res, file) => {
|
|||
};
|
||||
|
||||
const submit = async () => {
|
||||
loading.value = true;
|
||||
let success = 0;
|
||||
const files = uploaderFiles.value.slice();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const fileSize = file.size;
|
||||
|
||||
uploadHelper.value = i18n.global.t('file.fileUploadStart', [file.name]);
|
||||
if (fileSize <= 1024 * 1024 * 10) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file.raw);
|
||||
if (file.raw.webkitRelativePath != '') {
|
||||
formData.append('path', path.value + '/' + getPathWithoutFilename(file.raw.webkitRelativePath));
|
||||
} else {
|
||||
formData.append('path', path.value + '/' + getPathWithoutFilename(file.name));
|
||||
const fileNamesWithPath = Array.from(
|
||||
new Set(files.map((file) => `${path.value}/${file.raw.webkitRelativePath || file.name}`)),
|
||||
);
|
||||
const existFiles = await batchCheckFiles(fileNamesWithPath);
|
||||
if (existFiles.data.length > 0) {
|
||||
const fileSizeMap = new Map(
|
||||
files.map((file) => [`${path.value}/${file.raw.webkitRelativePath || file.name}`, file.size]),
|
||||
);
|
||||
existFiles.data.forEach((file) => {
|
||||
if (fileSizeMap.has(file.path)) {
|
||||
file.uploadSize = fileSizeMap.get(file.path);
|
||||
}
|
||||
formData.append('overwrite', 'True');
|
||||
uploadPercent.value = 0;
|
||||
await uploadFileData(formData, {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
|
||||
uploadPercent.value = progress;
|
||||
},
|
||||
timeout: 40000,
|
||||
});
|
||||
success++;
|
||||
uploaderFiles.value[i].status = 'success';
|
||||
} else {
|
||||
const CHUNK_SIZE = 1024 * 1024 * 5;
|
||||
const chunkCount = Math.ceil(fileSize / CHUNK_SIZE);
|
||||
let uploadedChunkCount = 0;
|
||||
for (let c = 0; c < chunkCount; c++) {
|
||||
const start = c * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, fileSize);
|
||||
const chunk = file.raw.slice(start, end);
|
||||
const formData = new FormData();
|
||||
});
|
||||
dialogExistFileRef.value.acceptParams({
|
||||
paths: existFiles.data,
|
||||
onConfirm: handleFileUpload,
|
||||
});
|
||||
} else {
|
||||
await uploadFile(files);
|
||||
}
|
||||
};
|
||||
|
||||
formData.append('filename', getFilenameFromPath(file.name));
|
||||
if (file.raw.webkitRelativePath != '') {
|
||||
formData.append('path', path.value + '/' + getPathWithoutFilename(file.raw.webkitRelativePath));
|
||||
} else {
|
||||
formData.append('path', path.value + '/' + getPathWithoutFilename(file.name));
|
||||
}
|
||||
formData.append('chunk', chunk);
|
||||
formData.append('chunkIndex', c.toString());
|
||||
formData.append('chunkCount', chunkCount.toString());
|
||||
const handleFileUpload = (action: 'skip' | 'overwrite', skippedPaths: string[] = []) => {
|
||||
const files = uploaderFiles.value.slice();
|
||||
if (action === 'skip') {
|
||||
const filteredFiles = files.filter(
|
||||
(file) => !skippedPaths.includes(`${path.value}/${file.raw.webkitRelativePath || file.name}`),
|
||||
);
|
||||
uploaderFiles.value = filteredFiles;
|
||||
uploadFile(filteredFiles);
|
||||
} else if (action === 'overwrite') {
|
||||
uploadFile(files);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await chunkUploadFileData(formData, {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const progress = Math.round(
|
||||
((uploadedChunkCount + progressEvent.loaded / progressEvent.total) * 100) / chunkCount,
|
||||
);
|
||||
uploadPercent.value = progress;
|
||||
},
|
||||
timeout: TimeoutEnum.T_60S,
|
||||
});
|
||||
uploadedChunkCount++;
|
||||
} catch (error) {
|
||||
uploaderFiles.value[i].status = 'fail';
|
||||
break;
|
||||
}
|
||||
if (uploadedChunkCount == chunkCount) {
|
||||
success++;
|
||||
uploaderFiles.value[i].status = 'success';
|
||||
break;
|
||||
}
|
||||
const uploadFile = async (files: any[]) => {
|
||||
if (files.length == 0) {
|
||||
clearFiles();
|
||||
} else {
|
||||
loading.value = true;
|
||||
let successCount = 0;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
uploadHelper.value = i18n.global.t('file.fileUploadStart', [file.name]);
|
||||
|
||||
let isSuccess =
|
||||
file.size <= MAX_SINGLE_FILE_SIZE ? await uploadSingleFile(file) : await uploadLargeFile(file);
|
||||
|
||||
if (isSuccess) {
|
||||
successCount++;
|
||||
uploaderFiles.value[i].status = 'success';
|
||||
} else {
|
||||
uploaderFiles.value[i].status = 'fail';
|
||||
}
|
||||
}
|
||||
|
||||
if (i == files.length - 1) {
|
||||
loading.value = false;
|
||||
uploadHelper.value = '';
|
||||
if (success == files.length) {
|
||||
clearFiles();
|
||||
MsgSuccess(i18n.global.t('file.uploadSuccess'));
|
||||
}
|
||||
loading.value = false;
|
||||
uploadHelper.value = '';
|
||||
|
||||
if (successCount === files.length) {
|
||||
clearFiles();
|
||||
MsgSuccess(i18n.global.t('file.uploadSuccess'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const uploadSingleFile = async (file: { raw: string | Blob }) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file.raw);
|
||||
formData.append('path', getUploadPath(file));
|
||||
formData.append('overwrite', 'True');
|
||||
uploadPercent.value = 0;
|
||||
await uploadFileData(formData, {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
uploadPercent.value = Math.round((progressEvent.loaded / progressEvent.total) * 100);
|
||||
},
|
||||
timeout: 40000,
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const uploadLargeFile = async (file: { size: any; raw: string | Blob; name: string }) => {
|
||||
const fileSize = file.size;
|
||||
const chunkCount = Math.ceil(fileSize / CHUNK_SIZE);
|
||||
let uploadedChunkCount = 0;
|
||||
for (let c = 0; c < chunkCount; c++) {
|
||||
const start = c * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, fileSize);
|
||||
const chunk = file.raw.slice(start, end);
|
||||
const formData = new FormData();
|
||||
formData.append('filename', getFilenameFromPath(file.name));
|
||||
formData.append('path', getUploadPath(file));
|
||||
formData.append('chunk', chunk);
|
||||
formData.append('chunkIndex', c.toString());
|
||||
formData.append('chunkCount', chunkCount.toString());
|
||||
|
||||
try {
|
||||
await chunkUploadFileData(formData, {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
uploadPercent.value = Math.round(
|
||||
((uploadedChunkCount + progressEvent.loaded / progressEvent.total) * 100) / chunkCount,
|
||||
);
|
||||
},
|
||||
timeout: TimeoutEnum.T_60S,
|
||||
});
|
||||
uploadedChunkCount++;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return uploadedChunkCount === chunkCount;
|
||||
};
|
||||
|
||||
const getUploadPath = (file) => {
|
||||
return `${path.value}/${getPathWithoutFilename(file.raw.webkitRelativePath || file.name)}`;
|
||||
};
|
||||
|
||||
const getPathWithoutFilename = (path: string) => {
|
||||
return path ? path.split('/').slice(0, -1).join('/') : path;
|
||||
};
|
||||
|
|
@ -358,8 +401,7 @@ const acceptParams = (props: UploadFileProps) => {
|
|||
uploadHelper.value = '';
|
||||
|
||||
nextTick(() => {
|
||||
const uploadEle = document.querySelector('.el-upload__input');
|
||||
state.uploadEle = uploadEle;
|
||||
state.uploadEle = document.querySelector('.el-upload__input');
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -294,9 +294,11 @@ const languageOptions = ref([
|
|||
...(!globalStore.isIntl ? [{ value: 'en', label: 'English' }] : []),
|
||||
{ value: 'ja', label: '日本語' },
|
||||
{ value: 'pt-BR', label: 'Português (Brasil)' },
|
||||
{ value: 'ko', label: '한국어' },
|
||||
{ value: 'ru', label: 'Русский' },
|
||||
{ value: 'ms', label: 'Bahasa Melayu' },
|
||||
]);
|
||||
|
||||
if (globalStore.isIntl) {
|
||||
languageOptions.value.unshift({ value: 'en', label: 'English' });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue