Skip to content

Commit af48fed

Browse files
authored
feat: Add file preview functionality and interface (#11200)
* feat: Add file content preview functionality - Implemented a new API endpoint for previewing file content. - Added PreviewContent method in BaseApi to handle requests. - Introduced GetPreviewContent method in FileService to retrieve file previews. - Updated frontend to include a TextPreview component for displaying file previews. - Added localization support for preview-related messages in multiple languages. - Enhanced file management view to support previewing large files. * feat: Update file preview functionality and interface - Added PreviewContentReq interface to define request parameters for file preview. - Updated getPreviewContent function to use the new PreviewContentReq type. - Modified text-preview component to align with updated API, removing unnecessary parameters.
1 parent ce3fa94 commit af48fed

File tree

18 files changed

+340
-2
lines changed

18 files changed

+340
-2
lines changed

agent/app/api/v2/file.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,29 @@ func (b *BaseApi) GetContent(c *gin.Context) {
272272
}
273273
}
274274

275+
// @Tags File
276+
// @Summary Preview file content
277+
// @Accept json
278+
// @Param request body request.FileContentReq true "request"
279+
// @Success 200 {object} response.FileInfo
280+
// @Security ApiKeyAuth
281+
// @Security Timestamp
282+
// @Router /files/preview [post]
283+
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"预览文件内容 [path]","formatEN":"Preview file content [path]"}
284+
func (b *BaseApi) PreviewContent(c *gin.Context) {
285+
var req request.FileContentReq
286+
if err := helper.CheckBindAndValidate(&req, c); err != nil {
287+
return
288+
}
289+
info, err := fileService.GetPreviewContent(req)
290+
if err != nil {
291+
helper.InternalServer(c, err)
292+
return
293+
}
294+
295+
helper.SuccessWithData(c, info)
296+
}
297+
275298
// @Tags File
276299
// @Summary Update file content
277300
// @Accept json

agent/app/dto/request/file.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ type FileContentReq struct {
1313
Path string `json:"path" validate:"required"`
1414
IsDetail bool `json:"isDetail"`
1515
}
16-
1716
type SearchUploadWithPage struct {
1817
dto.PageInfo
1918
Path string `json:"path" validate:"required"`

agent/app/service/file.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type IFileService interface {
5656
Compress(c request.FileCompress) error
5757
DeCompress(c request.FileDeCompress) error
5858
GetContent(op request.FileContentReq) (response.FileInfo, error)
59+
GetPreviewContent(op request.FileContentReq) (response.FileInfo, error)
5960
SaveContent(edit request.FileEdit) error
6061
FileDownload(d request.FileDownload) (string, error)
6162
DirSize(req request.DirSizeReq) (response.DirSizeRes, error)
@@ -374,6 +375,82 @@ func (f *FileService) GetContent(op request.FileContentReq) (response.FileInfo,
374375
return response.FileInfo{FileInfo: *info}, nil
375376
}
376377

378+
func (f *FileService) GetPreviewContent(op request.FileContentReq) (response.FileInfo, error) {
379+
info, err := files.NewFileInfo(files.FileOption{
380+
Path: op.Path,
381+
Expand: false,
382+
IsDetail: op.IsDetail,
383+
})
384+
if err != nil {
385+
return response.FileInfo{}, err
386+
}
387+
388+
if files.IsBlockDevice(info.FileMode) {
389+
return response.FileInfo{FileInfo: *info}, nil
390+
}
391+
392+
file, err := os.Open(op.Path)
393+
if err != nil {
394+
return response.FileInfo{}, err
395+
}
396+
defer file.Close()
397+
398+
headBuf := make([]byte, 1024)
399+
n, err := file.Read(headBuf)
400+
if err != nil && err != io.EOF {
401+
return response.FileInfo{}, err
402+
}
403+
headBuf = headBuf[:n]
404+
405+
if len(headBuf) > 0 && files.DetectBinary(headBuf) {
406+
return response.FileInfo{FileInfo: *info}, nil
407+
}
408+
409+
const maxSize = 10 * 1024 * 1024
410+
if info.Size <= maxSize {
411+
if _, err := file.Seek(0, 0); err != nil {
412+
return response.FileInfo{}, err
413+
}
414+
content, err := io.ReadAll(file)
415+
if err != nil {
416+
return response.FileInfo{}, err
417+
}
418+
info.Content = string(content)
419+
} else {
420+
lines, err := files.TailFromEnd(op.Path, 300)
421+
if err != nil {
422+
return response.FileInfo{}, err
423+
}
424+
info.Content = strings.Join(lines, "\n")
425+
}
426+
427+
content := []byte(info.Content)
428+
if len(content) > 1024 {
429+
content = content[:1024]
430+
}
431+
if !utf8.Valid(content) {
432+
_, decodeName, _ := charset.DetermineEncoding(content, "")
433+
decoder := files.GetDecoderByName(decodeName)
434+
if decoder != nil {
435+
reader := strings.NewReader(info.Content)
436+
var dec *encoding.Decoder
437+
if decodeName == "windows-1252" {
438+
dec = simplifiedchinese.GBK.NewDecoder()
439+
} else {
440+
dec = decoder.NewDecoder()
441+
}
442+
decodedReader := transform.NewReader(reader, dec)
443+
contents, err := io.ReadAll(decodedReader)
444+
if err != nil {
445+
return response.FileInfo{}, err
446+
}
447+
info.Content = string(contents)
448+
}
449+
}
450+
451+
return response.FileInfo{FileInfo: *info}, nil
452+
}
453+
377454
func (f *FileService) SaveContent(edit request.FileEdit) error {
378455
info, err := files.NewFileInfo(files.FileOption{
379456
Path: edit.Path,

agent/router/ro_file.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
2323
fileRouter.POST("/compress", baseApi.CompressFile)
2424
fileRouter.POST("/decompress", baseApi.DeCompressFile)
2525
fileRouter.POST("/content", baseApi.GetContent)
26+
fileRouter.POST("/preview", baseApi.PreviewContent)
2627
fileRouter.POST("/save", baseApi.SaveContent)
2728
fileRouter.POST("/check", baseApi.CheckFile)
2829
fileRouter.POST("/batch/check", baseApi.BatchCheckFiles)

frontend/src/api/interface/file.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ export namespace File {
4141
node: string;
4242
}
4343

44+
export interface PreviewContentReq {
45+
path: string;
46+
isDetail?: boolean;
47+
}
48+
4449
export interface SearchUploadInfo extends ReqPage {
4550
path: string;
4651
}

frontend/src/api/modules/files.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export const getFileContent = (params: File.ReqFile) => {
5454
return http.post<File.File>('files/content', params);
5555
};
5656

57+
export const getPreviewContent = (params: File.PreviewContentReq) => {
58+
return http.post<File.File>('files/preview', params, TimeoutEnum.T_5M);
59+
};
60+
5761
export const saveFileContent = (params: File.FileEdit) => {
5862
return http.post<File.File>('files/save', params);
5963
};

frontend/src/lang/modules/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1576,6 +1576,9 @@ const message = {
15761576
noNameFile: 'Untitled file',
15771577
minimap: 'Code mini map',
15781578
fileCanNotRead: 'File can not read',
1579+
previewTruncated: 'File is too large, only showing the last part',
1580+
previewEmpty: 'File is empty or not a text file',
1581+
previewLargeFile: 'Preview',
15791582
panelInstallDir: `1Panel installation directory can't be deleted`,
15801583
wgetTask: 'Download Task',
15811584
existFileTitle: 'Same name file prompt',

frontend/src/lang/modules/es-es.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,9 @@ const message = {
15781578
noNameFile: 'Archivo sin nombre',
15791579
minimap: 'Mapa de código',
15801580
fileCanNotRead: 'No se puede leer el archivo',
1581+
previewTruncated: 'El archivo es demasiado grande, solo se muestra la última parte',
1582+
previewEmpty: 'El archivo está vacío o no es un archivo de texto',
1583+
previewLargeFile: 'Vista previa',
15811584
panelInstallDir: 'El directorio de instalación de 1Panel no puede eliminarse',
15821585
wgetTask: 'Tarea de descarga',
15831586
existFileTitle: 'Archivo con el mismo nombre',

frontend/src/lang/modules/ja.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,6 +1527,9 @@ const message = {
15271527
noNameFile: '無題のファイル',
15281528
minimap: 'コードミニマップ',
15291529
fileCanNotRead: 'ファイルは読み取れません',
1530+
previewTruncated: 'ファイルが大きすぎるため、末尾の内容のみ表示しています',
1531+
previewEmpty: 'ファイルが空であるか、テキストファイルではありません',
1532+
previewLargeFile: 'プレビュー',
15301533
panelInstallDir: `1Panelインストールディレクトリは削除できません`,
15311534
wgetTask: 'ダウンロードタスク',
15321535
existFileTitle: '同名ファイルの警告',

frontend/src/lang/modules/ko.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,9 @@ const message = {
15091509
noNameFile: '제목 없는 파일',
15101510
minimap: '코드 미니맵',
15111511
fileCanNotRead: '파일을 읽을 수 없습니다.',
1512+
previewTruncated: '파일이 너무 커서 마지막 부분만 표시됩니다',
1513+
previewEmpty: '파일이 비어 있거나 텍스트 파일이 아닙니다',
1514+
previewLargeFile: '미리보기',
15121515
panelInstallDir: `1Panel 설치 디렉터리는 삭제할 수 없습니다.`,
15131516
wgetTask: '다운로드 작업',
15141517
existFileTitle: '동일한 이름의 파일 경고',

0 commit comments

Comments
 (0)