From 47d5e919a2d772b384b6d63bf083f6dba7c0e768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=98=AD?= <81747598+lan-yonghui@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:16:15 +0800 Subject: [PATCH] feat: Check for duplicate files before uploading/copying/moving (#8177) --- agent/app/api/v2/file.go | 17 ++ agent/app/dto/request/file.go | 4 + agent/app/dto/response/file.go | 8 + agent/app/service/file.go | 16 + agent/router/ro_file.go | 1 + core/app/dto/auth.go | 2 +- frontend/src/api/interface/file.ts | 8 + frontend/src/api/modules/files.ts | 4 + frontend/src/components/drawer-pro/index.vue | 2 + frontend/src/components/exist-file/index.vue | 78 +++++ frontend/src/lang/modules/en.ts | 7 + frontend/src/lang/modules/ja.ts | 15 +- frontend/src/lang/modules/ko.ts | 7 + frontend/src/lang/modules/ms.ts | 7 + frontend/src/lang/modules/pt-br.ts | 7 + frontend/src/lang/modules/ru.ts | 9 +- frontend/src/lang/modules/zh-Hant.ts | 7 + frontend/src/lang/modules/zh.ts | 7 + .../src/layout/components/Sidebar/index.vue | 1 + frontend/src/views/app-store/index.scss | 19 +- .../src/views/app-store/installed/index.vue | 276 ++++++++---------- .../src/views/host/file-management/index.vue | 18 +- .../views/host/file-management/move/index.vue | 140 ++++++++- .../host/file-management/upload/index.vue | 188 +++++++----- frontend/src/views/setting/panel/index.vue | 2 + 25 files changed, 602 insertions(+), 248 deletions(-) create mode 100644 frontend/src/components/exist-file/index.vue diff --git a/agent/app/api/v2/file.go b/agent/app/api/v2/file.go index 0e4b1c170..f4e27a76b 100644 --- a/agent/app/api/v2/file.go +++ b/agent/app/api/v2/file.go @@ -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 diff --git a/agent/app/dto/request/file.go b/agent/app/dto/request/file.go index 201d49ae3..fcb06f1a5 100644 --- a/agent/app/dto/request/file.go +++ b/agent/app/dto/request/file.go @@ -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"` diff --git a/agent/app/dto/response/file.go b/agent/app/dto/response/file.go index b087f81ba..8bf85b7f1 100644 --- a/agent/app/dto/response/file.go +++ b/agent/app/dto/response/file.go @@ -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"` +} diff --git a/agent/app/service/file.go b/agent/app/service/file.go index 7abef06dd..7176a06f3 100644 --- a/agent/app/service/file.go +++ b/agent/app/service/file.go @@ -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 +} diff --git a/agent/router/ro_file.go b/agent/router/ro_file.go index 8b6599c4c..8e24f0b95 100644 --- a/agent/router/ro_file.go +++ b/agent/router/ro_file.go @@ -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) diff --git a/core/app/dto/auth.go b/core/app/dto/auth.go index de2d5be88..a9af620a6 100644 --- a/core/app/dto/auth.go +++ b/core/app/dto/auth.go @@ -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 { diff --git a/frontend/src/api/interface/file.ts b/frontend/src/api/interface/file.ts index ac8e7827b..d7b34e00f 100644 --- a/frontend/src/api/interface/file.ts +++ b/frontend/src/api/interface/file.ts @@ -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; diff --git a/frontend/src/api/modules/files.ts b/frontend/src/api/modules/files.ts index 4e5ef181b..f64eec5f3 100644 --- a/frontend/src/api/modules/files.ts +++ b/frontend/src/api/modules/files.ts @@ -57,6 +57,10 @@ export const uploadFileData = (params: FormData, config: AxiosRequestConfig) => return http.upload('files/upload', params, config); }; +export const batchCheckFiles = (paths: string[]) => { + return http.post('files/batch/check', { paths: paths }, TimeoutEnum.T_5M); +}; + export const chunkUploadFileData = (params: FormData, config: AxiosRequestConfig) => { return http.upload('files/chunkupload', params, config); }; diff --git a/frontend/src/components/drawer-pro/index.vue b/frontend/src/components/drawer-pro/index.vue index 9cfb1c6e3..a8e61517f 100644 --- a/frontend/src/components/drawer-pro/index.vue +++ b/frontend/src/components/drawer-pro/index.vue @@ -87,6 +87,8 @@ const size = computed(() => { return '100%'; case '60%': return '60%'; + case props.size: + return props.size; default: return '50%'; } diff --git a/frontend/src/components/exist-file/index.vue b/frontend/src/components/exist-file/index.vue new file mode 100644 index 000000000..987a76e5e --- /dev/null +++ b/frontend/src/components/exist-file/index.vue @@ -0,0 +1,78 @@ + + diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index a15a0b3f0..0b1497e87 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -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', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 412cd3574..0ffa27247 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -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: '設定', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index a9add6095..e52f17d86 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -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: '설정', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index d8e511b0a..58194f56a 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -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', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 28cbb6edf..e18da7dde 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -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', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 5e1c05147..bd49e46cf 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -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: 'настройка', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 30d1e0c18..85570f67a 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -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: '開機自啟', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index dc189eb1e..aae43781f 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -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: '开机自启', diff --git a/frontend/src/layout/components/Sidebar/index.vue b/frontend/src/layout/components/Sidebar/index.vue index f4800ffe7..fc0c53c1f 100644 --- a/frontend/src/layout/components/Sidebar/index.vue +++ b/frontend/src/layout/components/Sidebar/index.vue @@ -55,6 +55,7 @@ :collapse-transition="false" :unique-opened="true" @select="handleMenuClick" + class="custom-menu" > diff --git a/frontend/src/views/app-store/index.scss b/frontend/src/views/app-store/index.scss index 9b33a9cba..03e4f45ec 100644 --- a/frontend/src/views/app-store/index.scss +++ b/frontend/src/views/app-store/index.scss @@ -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; diff --git a/frontend/src/views/app-store/installed/index.vue b/frontend/src/views/app-store/installed/index.vue index 94ed8275a..cffe45ad1 100644 --- a/frontend/src/views/app-store/installed/index.vue +++ b/frontend/src/views/app-store/installed/index.vue @@ -96,7 +96,6 @@ :md="24" :lg="12" :xl="12" - :class="mode === 'upgrade' ? 'upgrade-card-col-12' : 'install-card-col-12'" >
@@ -113,122 +112,128 @@
- - - {{ installed.name }} - - - - - - - - -
- {{ installed.message }} -
-
-
- - - - - - +
+
+ + + {{ installed.name }} + - - - - + + + + + + +
+ {{ installed.message }} +
+
+
+ + + + + + + + + + + + + + + + +
+
- + {{ $t('database.loadBackup') }} - - - - {{ $t('database.loadBackup') }} - - - {{ $t('commons.button.backup') }} - - - {{ $t('commons.button.ignore') }} - - - {{ $t('commons.button.upgrade') }} - + + {{ $t('commons.button.backup') }} + + + {{ $t('commons.button.ignore') }} + + + {{ $t('commons.button.upgrade') }} + +
+
- -
- - {{ $t('app.alreadyRun') }}{{ $t('commons.colon') }} - {{ getAge(installed.createdAt) }} - -
+
+
+ + {{ $t('app.alreadyRun') }}{{ $t('commons.colon') }} + {{ getAge(installed.createdAt) }} +
{ diff --git a/frontend/src/views/host/file-management/upload/index.vue b/frontend/src/views/host/file-management/upload/index.vue index afda5ff36..5eeb9674d 100644 --- a/frontend/src/views/host/file-management/upload/index.vue +++ b/frontend/src/views/host/file-management/upload/index.vue @@ -91,16 +91,18 @@ +