From 8bb225d27250b4afb921557721c45035af3ca00c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=98=AD?= <81747598+lan-yonghui@users.noreply.github.com> Date: Tue, 11 Feb 2025 18:11:48 +0800 Subject: [PATCH] feat: Check for duplicate files before uploading (#7849) Refs #5893 --- backend/app/api/v1/file.go | 17 ++ backend/app/dto/request/file.go | 4 + backend/app/dto/response/file.go | 8 + backend/app/service/file.go | 16 ++ backend/router/ro_file.go | 1 + frontend/src/api/interface/file.ts | 8 + frontend/src/api/modules/files.ts | 4 + frontend/src/components/exist-file/index.vue | 78 ++++++++ frontend/src/lang/modules/en.ts | 6 + frontend/src/lang/modules/ja.ts | 6 + frontend/src/lang/modules/ko.ts | 6 + frontend/src/lang/modules/ms.ts | 6 + frontend/src/lang/modules/pt-br.ts | 6 + frontend/src/lang/modules/ru.ts | 7 + frontend/src/lang/modules/tw.ts | 6 + frontend/src/lang/modules/zh.ts | 6 + .../host/file-management/upload/index.vue | 188 +++++++++++------- 17 files changed, 300 insertions(+), 73 deletions(-) create mode 100644 frontend/src/components/exist-file/index.vue diff --git a/backend/app/api/v1/file.go b/backend/app/api/v1/file.go index 1a7914177..dae54cfe8 100644 --- a/backend/app/api/v1/file.go +++ b/backend/app/api/v1/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/backend/app/dto/request/file.go b/backend/app/dto/request/file.go index 96c771dab..6a31b611a 100644 --- a/backend/app/dto/request/file.go +++ b/backend/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/backend/app/dto/response/file.go b/backend/app/dto/response/file.go index 977f7b148..73a541767 100644 --- a/backend/app/dto/response/file.go +++ b/backend/app/dto/response/file.go @@ -2,6 +2,7 @@ package response import ( "github.com/1Panel-dev/1Panel/backend/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 float64 `json:"size"` + ModTime time.Time `json:"modTime"` +} diff --git a/backend/app/service/file.go b/backend/app/service/file.go index 44164b67e..79acdc250 100644 --- a/backend/app/service/file.go +++ b/backend/app/service/file.go @@ -50,6 +50,7 @@ type IFileService interface { ChangeMode(op request.FileCreate) error BatchChangeModeAndOwner(op request.FileRoleReq) error ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error) + BatchCheckFiles(req request.FilePathsCheck) []response.ExistFileInfo } var filteredPaths = []string{ @@ -501,3 +502,18 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi } return res, nil } + +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: float64(info.Size()), + Name: info.Name(), + Path: filePath, + ModTime: info.ModTime(), + }) + } + } + return fileList +} diff --git a/backend/router/ro_file.go b/backend/router/ro_file.go index a70307aaf..6dec4a4a4 100644 --- a/backend/router/ro_file.go +++ b/backend/router/ro_file.go @@ -27,6 +27,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/frontend/src/api/interface/file.ts b/frontend/src/api/interface/file.ts index 060ce7441..487b994c5 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 f1d438e19..c914669e3 100644 --- a/frontend/src/api/modules/files.ts +++ b/frontend/src/api/modules/files.ts @@ -53,6 +53,10 @@ export const CheckFile = (path: string) => { return http.post('files/check', { path: path }); }; +export const BatchCheckFiles = (paths: string[]) => { + return http.post('files/batch/check', { paths: paths }, TimeoutEnum.T_5M); +}; + export const UploadFileData = (params: FormData, config: AxiosRequestConfig) => { return http.upload('files/upload', params, config); }; 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 1668a50a8..47b9a0cfe 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -74,6 +74,8 @@ const message = { createNewFile: 'Create new file', helpDoc: 'Help Document', unbind: 'Unbind', + cover: 'cover', + skip: 'skip', }, search: { timeStart: 'Time start', @@ -110,6 +112,7 @@ const message = { refreshRate: 'Refresh rate', refreshRateUnit: 'No refresh | {n} second/time | {n} seconds/time', selectColumn: 'Select column', + serialNumber: 'Serial number', }, loadingText: { Upgrading: 'System upgrade, please wait...', @@ -1320,6 +1323,9 @@ const message = { minimap: 'Code mini map', fileCanNotRead: 'File can not read', panelInstallDir: `1Panel installation directory can't be deleted`, + 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)', }, ssh: { setting: 'Setting', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index a0b3de278..903a14414 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -73,6 +73,8 @@ const message = { createNewFile: '新しいファイルを作成します', helpDoc: '文書をヘルプします', unbind: 'バインド', + cover: '上書き', + skip: 'スキップ', }, search: { timeStart: '時間開始', @@ -109,6 +111,7 @@ const message = { refreshRate: 'リフレッシュレート', refreshRateUnit: '更新なし|{n}秒/時間 |{n}秒/時間', selectColumn: '列を選択します', + serialNumber: 'シリアル番号', }, loadingText: { Upgrading: 'システムのアップグレード、待ってください...', @@ -1298,6 +1301,9 @@ const message = { minimap: 'コードミニマップ', fileCanNotRead: 'ファイルは読み取れません', panelInstallDir: `1Panelインストールディレクトリは削除できません`, + existFileTitle: '同名ファイルの警告', + existFileHelper: 'アップロードしたファイルに同じ名前のファイルが含まれています。上書きしますか?', + existFileSize: 'ファイルサイズ(新しい -> 古い)', }, ssh: { setting: '設定', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index d4dc7e74d..fb9140dbf 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -74,6 +74,8 @@ const message = { createNewFile: '새 파일 생성', helpDoc: '도움말 문서', unbind: '연결 해제', + cover: '덮어쓰기', + skip: '건너뛰기', }, search: { timeStart: '시작 시간', @@ -110,6 +112,7 @@ const message = { refreshRate: '새로 고침 속도', refreshRateUnit: '새로 고침 안 함 | {n} 초/회 | {n} 초/회', selectColumn: '열 선택', + serialNumber: '일련 번호', }, loadingText: { Upgrading: '시스템 업그레이드 중입니다. 잠시만 기다려 주십시오...', @@ -1285,6 +1288,9 @@ const message = { minimap: '코드 미니맵', fileCanNotRead: '파일을 읽을 수 없습니다.', panelInstallDir: `1Panel 설치 디렉터리는 삭제할 수 없습니다.`, + existFileTitle: '동일한 이름의 파일 경고', + existFileHelper: '업로드한 파일에 동일한 이름의 파일이 포함되어 있습니다. 덮어쓰시겠습니까?', + existFileSize: '파일 크기 (새로운 -> 오래된)', }, ssh: { setting: '설정', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 00402ee9c..c8023125e 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -74,6 +74,8 @@ const message = { createNewFile: 'Cipta fail baru', helpDoc: 'Dokumen Bantuan', unbind: 'Nyahkaitkan', + cover: 'Tindih', + skip: 'Langkau', }, search: { timeStart: 'Masa mula', @@ -110,6 +112,7 @@ const message = { refreshRate: 'Kadar penyegaran', refreshRateUnit: 'Tiada penyegaran | {n} saat/masa | {n} saat/masa', selectColumn: 'Pilih lajur', + serialNumber: 'Nombor siri', }, loadingText: { Upgrading: 'Peningkatan sistem, sila tunggu...', @@ -1340,6 +1343,9 @@ const message = { minimap: 'Peta mini kod', fileCanNotRead: 'Fail tidak dapat dibaca', panelInstallDir: 'Direktori pemasangan 1Panel tidak boleh dipadamkan', + 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)', }, ssh: { setting: 'tetapan', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index e4d30c3a6..173130e5c 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -74,6 +74,8 @@ const message = { createNewFile: 'Criar novo arquivo', helpDoc: 'Documento de ajuda', unbind: 'Desvincular', + cover: 'Substituir', + skip: 'Pular', }, search: { timeStart: 'Hora inicial', @@ -110,6 +112,7 @@ const message = { refreshRate: 'Taxa de atualização', refreshRateUnit: 'Sem atualização | {n} segundo/atualização | {n} segundos/atualização', selectColumn: 'Selecionar coluna', + serialNumber: 'Número de série', }, loadingText: { Upgrading: 'Atualizando o sistema, por favor, aguarde...', @@ -1326,6 +1329,9 @@ const message = { minimap: 'Mini mapa de código', fileCanNotRead: 'O arquivo não pode ser lido', panelInstallDir: 'O diretório de instalação do 1Panel não pode ser excluído', + 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)', }, ssh: { setting: 'configuração', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 2f4b3e744..24df3e3b2 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -74,6 +74,8 @@ const message = { createNewFile: 'Создать новый файл', helpDoc: 'Справка', unbind: 'Отвязать', + cover: 'Заменить', + skip: 'Пропустить', }, search: { timeStart: 'Время начала', @@ -110,6 +112,8 @@ const message = { refreshRate: 'Частота обновления', refreshRateUnit: 'Без обновления | {n} секунда/раз | {n} секунд/раз', selectColumn: 'Выбрать столбец', + cover: 'Заменить', + skip: 'Пропустить', }, loadingText: { Upgrading: 'Обновление системы, пожалуйста, подождите...', @@ -1328,6 +1332,9 @@ const message = { minimap: 'Мини-карта кода', fileCanNotRead: 'Файл не может быть прочитан', panelInstallDir: 'Директорию установки 1Panel нельзя удалить', + existFileTitle: 'Предупреждение о файле с тем же именем', + existFileHelper: 'Загруженный файл содержит файл с таким же именем. Заменить его?', + existFileSize: 'Размер файла (новый -> старый)', }, ssh: { setting: 'настройка', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index 6f394b920..7639eb539 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -73,6 +73,8 @@ const message = { createNewFile: '新增檔案', helpDoc: '說明文件', unbind: '解綁', + cover: '覆蓋', + skip: '跳過', }, search: { timeStart: '開始時間', @@ -110,6 +112,7 @@ const message = { noRefresh: '不更新', refreshRateUnit: '不更新 | {0} 秒/次 | {0} 秒/次', selectColumn: '選擇列', + serialNumber: '序號', }, loadingText: { Upgrading: '系統升級中,請稍候...', @@ -1257,6 +1260,9 @@ const message = { minimap: '縮圖', fileCanNotRead: '此檔案不支援預覽', panelInstallDir: '1Panel 安裝目錄不能刪除', + existFileTitle: '同名檔案提示', + existFileHelper: '上傳的檔案存在同名檔案,是否覆蓋?', + existFileSize: '文件大小(新->舊)', }, ssh: { setting: '設定', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index a095557ae..9e7c56120 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -73,6 +73,8 @@ const message = { createNewFile: '新建文件', helpDoc: '帮助文档', unbind: '解绑', + cover: '覆盖', + skip: '跳过', }, search: { timeStart: '开始时间', @@ -109,6 +111,7 @@ const message = { refreshRate: '刷新频率', refreshRateUnit: '不刷新 | {n} 秒/次 | {n} 秒/次', selectColumn: '选择列', + serialNumber: '序号', }, loadingText: { Upgrading: '系统升级中,请稍候...', @@ -1258,6 +1261,9 @@ const message = { minimap: '缩略图', fileCanNotRead: '此文件不支持预览', panelInstallDir: '1Panel 安装目录不能删除', + existFileTitle: '同名文件提示', + existFileHelper: '上传的文件存在同名文件,是否覆盖?', + existFileSize: '文件大小 (新 -> 旧)', }, ssh: { setting: '配置', diff --git a/frontend/src/views/host/file-management/upload/index.vue b/frontend/src/views/host/file-management/upload/index.vue index 3b0d817d9..cc650b1eb 100644 --- a/frontend/src/views/host/file-management/upload/index.vue +++ b/frontend/src/views/host/file-management/upload/index.vue @@ -85,18 +85,20 @@ +