mirror of
				https://github.com/veops/oneterm.git
				synced 2025-11-01 03:12:39 +08:00 
			
		
		
		
	perf(backend): file api for ls and download
This commit is contained in:
		| @@ -5,7 +5,11 @@ import ( | |||||||
| 	"io" | 	"io" | ||||||
| 	"io/fs" | 	"io/fs" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/samber/lo" | 	"github.com/samber/lo" | ||||||
| @@ -13,13 +17,43 @@ import ( | |||||||
| 	"go.uber.org/zap" | 	"go.uber.org/zap" | ||||||
|  |  | ||||||
| 	"github.com/veops/oneterm/internal/acl" | 	"github.com/veops/oneterm/internal/acl" | ||||||
|  | 	"github.com/veops/oneterm/internal/guacd" | ||||||
| 	"github.com/veops/oneterm/internal/model" | 	"github.com/veops/oneterm/internal/model" | ||||||
|  | 	"github.com/veops/oneterm/internal/repository" | ||||||
| 	"github.com/veops/oneterm/internal/service" | 	"github.com/veops/oneterm/internal/service" | ||||||
| 	gsession "github.com/veops/oneterm/internal/session" | 	gsession "github.com/veops/oneterm/internal/session" | ||||||
|  | 	dbpkg "github.com/veops/oneterm/pkg/db" | ||||||
| 	"github.com/veops/oneterm/pkg/errors" | 	"github.com/veops/oneterm/pkg/errors" | ||||||
| 	"github.com/veops/oneterm/pkg/logger" | 	"github.com/veops/oneterm/pkg/logger" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | func isPermissionError(err error) bool { | ||||||
|  | 	if err == nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	errStr := strings.ToLower(err.Error()) | ||||||
|  | 	permissionKeywords := []string{ | ||||||
|  | 		"permission denied", | ||||||
|  | 		"access denied", | ||||||
|  | 		"unauthorized", | ||||||
|  | 		"forbidden", | ||||||
|  | 		"not authorized", | ||||||
|  | 		"insufficient privileges", | ||||||
|  | 		"operation not permitted", | ||||||
|  | 		"sftp: permission denied", | ||||||
|  | 		"ssh: permission denied", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, keyword := range permissionKeywords { | ||||||
|  | 		if strings.Contains(errStr, keyword) { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
| // GetFileHistory godoc | // GetFileHistory godoc | ||||||
| // | // | ||||||
| //	@Tags		file | //	@Tags		file | ||||||
| @@ -31,13 +65,13 @@ import ( | |||||||
| //	@Param		end			query		string	false	"end, RFC3339" | //	@Param		end			query		string	false	"end, RFC3339" | ||||||
| //	@Param		uid			query		int		false	"uid" | //	@Param		uid			query		int		false	"uid" | ||||||
| //	@Param		asset_id	query		int		false	"asset id" | //	@Param		asset_id	query		int		false	"asset id" | ||||||
| //	@Param		accout_id	query		int		false	"account id" | //	@Param		account_id	query		int		false	"account id" | ||||||
| //	@Param		client_ip	query		string	false	"client_ip" | //	@Param		client_ip	query		string	false	"client_ip" | ||||||
| //	@Success	200			{object}	HttpResponse{data=ListData{list=[]model.Session}} | //	@Success	200			{object}	HttpResponse{data=ListData{list=[]model.Session}} | ||||||
| //	@Router		/file/history [get] | //	@Router		/file/history [get] | ||||||
| func (c *Controller) GetFileHistory(ctx *gin.Context) { | func (c *Controller) GetFileHistory(ctx *gin.Context) { | ||||||
| 	// Create filter conditions | 	// Create filter conditions | ||||||
| 	filters := make(map[string]interface{}) | 	filters := make(map[string]any) | ||||||
|  |  | ||||||
| 	// Get user permissions | 	// Get user permissions | ||||||
| 	currentUser, _ := acl.GetSessionFromCtx(ctx) | 	currentUser, _ := acl.GetSessionFromCtx(ctx) | ||||||
| @@ -112,8 +146,9 @@ func (c *Controller) GetFileHistory(ctx *gin.Context) { | |||||||
| //	@Param		asset_id	path		int		true	"asset_id" | //	@Param		asset_id	path		int		true	"asset_id" | ||||||
| //	@Param		account_id	path		int		true	"account_id" | //	@Param		account_id	path		int		true	"account_id" | ||||||
| //	@Param		dir			query		string	true	"dir" | //	@Param		dir			query		string	true	"dir" | ||||||
|  | //	@Param		show_hidden	query		bool	false	"show hidden files (default: false)" | ||||||
| //	@Success	200			{object}	HttpResponse | //	@Success	200			{object}	HttpResponse | ||||||
| //	@Router		/file/ls/:asset_id/:account_id [post] | //	@Router		/file/ls/:asset_id/:account_id [GET] | ||||||
| func (c *Controller) FileLS(ctx *gin.Context) { | func (c *Controller) FileLS(ctx *gin.Context) { | ||||||
| 	sess := &gsession.Session{ | 	sess := &gsession.Session{ | ||||||
| 		Session: &model.Session{ | 		Session: &model.Session{ | ||||||
| @@ -133,27 +168,52 @@ func (c *Controller) FileLS(ctx *gin.Context) { | |||||||
| 	// Use global file service | 	// Use global file service | ||||||
| 	info, err := service.DefaultFileService.ReadDir(ctx, sess.Session.AssetId, sess.Session.AccountId, ctx.Query("dir")) | 	info, err := service.DefaultFileService.ReadDir(ctx, sess.Session.AssetId, sess.Session.AccountId, ctx.Query("dir")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.AbortWithError(http.StatusBadRequest, &errors.ApiError{Code: errors.ErrInvalidArgument, Data: map[string]any{"err": err}}) | 		if isPermissionError(err) { | ||||||
|  | 			ctx.AbortWithError(http.StatusForbidden, fmt.Errorf("permission denied")) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.AbortWithError(http.StatusBadRequest, &errors.ApiError{Code: errors.ErrInvalidArgument, Data: map[string]any{"err": err}}) | ||||||
|  | 		} | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Filter hidden files unless show_hidden is true | ||||||
|  | 	showHidden := cast.ToBool(ctx.Query("show_hidden")) | ||||||
|  | 	if !showHidden { | ||||||
|  | 		info = lo.Filter(info, func(f fs.FileInfo, _ int) bool { | ||||||
|  | 			return !strings.HasPrefix(f.Name(), ".") | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	res := &ListData{ | 	res := &ListData{ | ||||||
| 		Count: int64(len(info)), | 		Count: int64(len(info)), | ||||||
| 		List: lo.Map(info, func(f fs.FileInfo, _ int) any { | 		List: lo.Map(info, func(f fs.FileInfo, _ int) any { | ||||||
|  | 			var target string | ||||||
|  | 			if f.Mode()&os.ModeSymlink != 0 { | ||||||
|  | 				cli, err := service.GetFileManager().GetFileClient(sess.Session.AssetId, sess.Session.AccountId) | ||||||
|  | 				if err == nil { | ||||||
|  | 					linkPath := filepath.Join(ctx.Query("dir"), f.Name()) | ||||||
|  | 					if linkTarget, err := cli.ReadLink(linkPath); err == nil { | ||||||
|  | 						target = linkTarget | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
| 			return &service.FileInfo{ | 			return &service.FileInfo{ | ||||||
| 				Name:  f.Name(), | 				Name:    f.Name(), | ||||||
| 				IsDir: f.IsDir(), | 				IsDir:   f.IsDir(), | ||||||
| 				Size:  f.Size(), | 				Size:    f.Size(), | ||||||
| 				Mode:  f.Mode().String(), | 				Mode:    f.Mode().String(), | ||||||
|  | 				IsLink:  f.Mode()&os.ModeSymlink != 0, | ||||||
|  | 				Target:  target, | ||||||
|  | 				ModTime: f.ModTime().Format(time.RFC3339), | ||||||
| 			} | 			} | ||||||
| 		}), | 		}), | ||||||
| 	} | 	} | ||||||
| 	ctx.JSON(http.StatusOK, NewHttpResponseWithData(res)) | 	ctx.JSON(http.StatusOK, NewHttpResponseWithData(res)) | ||||||
| } | } | ||||||
|  |  | ||||||
| // FileMkdir file | // FileMkdir godoc | ||||||
| // | // | ||||||
| //	@Tags		account | //	@Tags		file | ||||||
| //	@Param		asset_id	path		int		true	"asset_id" | //	@Param		asset_id	path		int		true	"asset_id" | ||||||
| //	@Param		account_id	path		int		true	"account_id" | //	@Param		account_id	path		int		true	"account_id" | ||||||
| //	@Param		dir			query		string	true	"dir " | //	@Param		dir			query		string	true	"dir " | ||||||
| @@ -276,8 +336,7 @@ func (c *Controller) FileUpload(ctx *gin.Context) { | |||||||
| //	@Param		asset_id	path		int		true	"asset_id" | //	@Param		asset_id	path		int		true	"asset_id" | ||||||
| //	@Param		account_id	path		int		true	"account_id" | //	@Param		account_id	path		int		true	"account_id" | ||||||
| //	@Param		dir			query		string	true	"dir" | //	@Param		dir			query		string	true	"dir" | ||||||
| //	@Param		filename	query		string	true	"filename" | //	@Param		names	query		string	true	"names (comma-separated for multiple files)" | ||||||
| //	@Param		file		formData	string	true	"file field name" |  | ||||||
| //	@Success	200			{object}	HttpResponse | //	@Success	200			{object}	HttpResponse | ||||||
| //	@Router		/file/download/:asset_id/:account_id [get] | //	@Router		/file/download/:asset_id/:account_id [get] | ||||||
| func (c *Controller) FileDownload(ctx *gin.Context) { | func (c *Controller) FileDownload(ctx *gin.Context) { | ||||||
| @@ -298,22 +357,38 @@ func (c *Controller) FileDownload(ctx *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Use global file service | 	filenameParam := ctx.Query("names") | ||||||
| 	rf, err := service.DefaultFileService.Open(ctx, sess.Session.AssetId, sess.Session.AccountId, filepath.Join(ctx.Query("dir"), ctx.Query("filename"))) | 	if filenameParam == "" { | ||||||
| 	if err != nil { | 		ctx.AbortWithError(http.StatusBadRequest, &errors.ApiError{Code: errors.ErrInvalidArgument, Data: map[string]any{"err": "names parameter is required"}}) | ||||||
| 		ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInvalidArgument, Data: map[string]any{"err": err}}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	defer rf.Close() | 	filenames := lo.Filter( | ||||||
|  | 		lo.Map(strings.Split(filenameParam, ","), func(name string, _ int) string { | ||||||
|  | 			return strings.TrimSpace(name) | ||||||
|  | 		}), | ||||||
|  | 		func(name string, _ int) bool { | ||||||
|  | 			return name != "" | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  |  | ||||||
| 	content, err := io.ReadAll(rf) | 	if len(filenames) == 0 { | ||||||
| 	if err != nil { | 		ctx.AbortWithError(http.StatusBadRequest, &errors.ApiError{Code: errors.ErrInvalidArgument, Data: map[string]any{"err": "no valid filenames provided"}}) | ||||||
| 		ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInternal, Data: map[string]any{"err": err}}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Create history record | 	reader, downloadFilename, fileSize, err := service.DefaultFileService.DownloadMultiple(ctx, sess.Session.AssetId, sess.Session.AccountId, ctx.Query("dir"), filenames) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if isPermissionError(err) { | ||||||
|  | 			ctx.AbortWithError(http.StatusForbidden, &errors.ApiError{Code: errors.ErrNoPerm, Data: map[string]any{"err": err}}) | ||||||
|  | 		} else { | ||||||
|  | 			ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInvalidArgument, Data: map[string]any{"err": err}}) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	defer reader.Close() | ||||||
|  |  | ||||||
|  | 	// Record file operation history | ||||||
| 	h := &model.FileHistory{ | 	h := &model.FileHistory{ | ||||||
| 		Uid:       currentUser.GetUid(), | 		Uid:       currentUser.GetUid(), | ||||||
| 		UserName:  currentUser.GetUserName(), | 		UserName:  currentUser.GetUserName(), | ||||||
| @@ -322,16 +397,421 @@ func (c *Controller) FileDownload(ctx *gin.Context) { | |||||||
| 		ClientIp:  ctx.ClientIP(), | 		ClientIp:  ctx.ClientIP(), | ||||||
| 		Action:    model.FILE_ACTION_DOWNLOAD, | 		Action:    model.FILE_ACTION_DOWNLOAD, | ||||||
| 		Dir:       ctx.Query("dir"), | 		Dir:       ctx.Query("dir"), | ||||||
| 		Filename:  ctx.Query("filename"), | 		Filename:  strings.Join(filenames, ","), | ||||||
|  | 		CreatedAt: time.Now(), | ||||||
|  | 	} | ||||||
|  | 	service.DefaultFileService.AddFileHistory(ctx, h) | ||||||
|  |  | ||||||
|  | 	// Set response headers for file download | ||||||
|  | 	ctx.Header("Content-Type", "application/octet-stream") | ||||||
|  | 	ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFilename)) | ||||||
|  | 	ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate") | ||||||
|  | 	ctx.Header("Pragma", "no-cache") | ||||||
|  | 	ctx.Header("Expires", "0") | ||||||
|  |  | ||||||
|  | 	// Set content length if known | ||||||
|  | 	if fileSize > 0 { | ||||||
|  | 		ctx.Header("Content-Length", fmt.Sprintf("%d", fileSize)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err = service.DefaultFileService.AddFileHistory(ctx, h); err != nil { | 	// Stream file content directly to response | ||||||
| 		logger.L().Error("record download failed", zap.Error(err), zap.Any("history", h)) | 	ctx.Status(http.StatusOK) | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	rw := ctx.Writer | 	// Use streaming copy with buffer to handle large files efficiently | ||||||
| 	rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", ctx.Query("filename"))) | 	buffer := make([]byte, 32*1024) // 32KB buffer for optimal performance | ||||||
| 	rw.Header().Set("Content-Type", "application/octet-stream") | 	_, err = io.CopyBuffer(ctx.Writer, reader, buffer) | ||||||
| 	rw.Header().Set("Content-Length", fmt.Sprintf("%d", len(content))) | 	if err != nil { | ||||||
| 	rw.Write(content) | 		logger.L().Error("File transfer failed", zap.Error(err)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RDP File Transfer Methods | ||||||
|  |  | ||||||
|  | // RDPFileInfo represents file information for RDP sessions | ||||||
|  | type RDPFileInfo struct { | ||||||
|  | 	Name    string `json:"name"` | ||||||
|  | 	Size    int64  `json:"size"` | ||||||
|  | 	IsDir   bool   `json:"is_dir"` | ||||||
|  | 	ModTime string `json:"mod_time"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RDPMkdirRequest represents directory creation request for RDP | ||||||
|  | type RDPMkdirRequest struct { | ||||||
|  | 	Path string `json:"path" binding:"required"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RDPFileList lists files in RDP session drive | ||||||
|  | // @Summary List RDP session files | ||||||
|  | // @Description Get file list for RDP session drive | ||||||
|  | // @Tags RDP File | ||||||
|  | // @Param session_id path string true "Session ID" | ||||||
|  | // @Param path query string false "Directory path" | ||||||
|  | // @Success	200			{object}	HttpResponse | ||||||
|  | // @Router /api/v1/rdp/sessions/{session_id}/files [get] | ||||||
|  | func (c *Controller) RDPFileList(ctx *gin.Context) { | ||||||
|  | 	sessionId := ctx.Param("session_id") | ||||||
|  | 	path := ctx.DefaultQuery("path", "/") | ||||||
|  |  | ||||||
|  | 	// Check if session exists and user has permission | ||||||
|  | 	if !c.hasRDPSessionPermission(ctx, sessionId) { | ||||||
|  | 		ctx.JSON(http.StatusForbidden, HttpResponse{ | ||||||
|  | 			Code:    http.StatusForbidden, | ||||||
|  | 			Message: "No permission to access this session", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get session tunnel | ||||||
|  | 	tunnel := c.getRDPSessionTunnel(sessionId) | ||||||
|  | 	if tunnel == nil { | ||||||
|  | 		ctx.JSON(http.StatusNotFound, HttpResponse{ | ||||||
|  | 			Code:    http.StatusNotFound, | ||||||
|  | 			Message: "Session not found or not active", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check if RDP drive is enabled | ||||||
|  | 	if !c.isRDPDriveEnabled(tunnel) { | ||||||
|  | 		ctx.JSON(http.StatusBadRequest, HttpResponse{ | ||||||
|  | 			Code:    http.StatusBadRequest, | ||||||
|  | 			Message: "RDP drive is not enabled for this session", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Send file list request through Guacamole protocol | ||||||
|  | 	files, err := c.requestRDPFileList(tunnel, path) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.L().Error("Failed to get RDP file list", zap.Error(err)) | ||||||
|  | 		ctx.JSON(http.StatusInternalServerError, HttpResponse{ | ||||||
|  | 			Code:    http.StatusInternalServerError, | ||||||
|  | 			Message: "Failed to get file list", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.JSON(http.StatusOK, HttpResponse{ | ||||||
|  | 		Code:    0, | ||||||
|  | 		Message: "ok", | ||||||
|  | 		Data:    files, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RDPFileUpload uploads file to RDP session drive | ||||||
|  | // @Summary Upload file to RDP session | ||||||
|  | // @Description Upload file to RDP session drive | ||||||
|  | // @Tags RDP File | ||||||
|  | // @Accept multipart/form-data | ||||||
|  | // @Param session_id path string true "Session ID" | ||||||
|  | // @Param file formData file true "File to upload" | ||||||
|  | // @Param path formData string false "Target directory path" | ||||||
|  | // @Success 200 {object} HttpResponse | ||||||
|  | // @Router /api/v1/rdp/sessions/{session_id}/files/upload [post] | ||||||
|  | func (c *Controller) RDPFileUpload(ctx *gin.Context) { | ||||||
|  | 	sessionId := ctx.Param("session_id") | ||||||
|  | 	targetPath := ctx.DefaultPostForm("path", "/") | ||||||
|  |  | ||||||
|  | 	// Check permission | ||||||
|  | 	if !c.hasRDPSessionPermission(ctx, sessionId) { | ||||||
|  | 		ctx.JSON(http.StatusForbidden, HttpResponse{ | ||||||
|  | 			Code:    http.StatusForbidden, | ||||||
|  | 			Message: "No permission to access this session", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get uploaded file | ||||||
|  | 	file, header, err := ctx.Request.FormFile("file") | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.JSON(http.StatusBadRequest, HttpResponse{ | ||||||
|  | 			Code:    http.StatusBadRequest, | ||||||
|  | 			Message: "Failed to get uploaded file", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	defer file.Close() | ||||||
|  |  | ||||||
|  | 	// Get session tunnel | ||||||
|  | 	tunnel := c.getRDPSessionTunnel(sessionId) | ||||||
|  | 	if tunnel == nil { | ||||||
|  | 		ctx.JSON(http.StatusNotFound, HttpResponse{ | ||||||
|  | 			Code:    http.StatusNotFound, | ||||||
|  | 			Message: "Session not found or not active", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check if upload is allowed | ||||||
|  | 	if !c.isRDPUploadAllowed(tunnel) { | ||||||
|  | 		ctx.JSON(http.StatusForbidden, HttpResponse{ | ||||||
|  | 			Code:    http.StatusForbidden, | ||||||
|  | 			Message: "File upload is disabled for this session", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Read file content | ||||||
|  | 	content, err := io.ReadAll(file) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.JSON(http.StatusInternalServerError, HttpResponse{ | ||||||
|  | 			Code:    http.StatusInternalServerError, | ||||||
|  | 			Message: "Failed to read file content", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Send upload request through Guacamole protocol | ||||||
|  | 	fullPath := filepath.Join(targetPath, header.Filename) | ||||||
|  | 	err = c.uploadRDPFile(tunnel, fullPath, content) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.L().Error("Failed to upload file to RDP session", zap.Error(err)) | ||||||
|  | 		ctx.JSON(http.StatusInternalServerError, HttpResponse{ | ||||||
|  | 			Code:    http.StatusInternalServerError, | ||||||
|  | 			Message: "Failed to upload file", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Record file operation history | ||||||
|  | 	c.recordRDPFileHistory(ctx, sessionId, "upload", fullPath, int64(len(content))) | ||||||
|  |  | ||||||
|  | 	ctx.JSON(http.StatusOK, HttpResponse{ | ||||||
|  | 		Code:    0, | ||||||
|  | 		Message: "ok", | ||||||
|  | 		Data: gin.H{ | ||||||
|  | 			"message": "File uploaded successfully", | ||||||
|  | 			"path":    fullPath, | ||||||
|  | 			"size":    len(content), | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RDPFileDownload downloads file from RDP session drive | ||||||
|  | // @Summary Download file from RDP session | ||||||
|  | // @Description Download file from RDP session drive | ||||||
|  | // @Tags RDP File | ||||||
|  | // @Accept json | ||||||
|  | // @Produce application/octet-stream | ||||||
|  | // @Param session_id path string true "Session ID" | ||||||
|  | // @Param path query string true "File path" | ||||||
|  | // @Success 200 {file} binary | ||||||
|  | // @Router /api/v1/rdp/sessions/{session_id}/files/download [get] | ||||||
|  | func (c *Controller) RDPFileDownload(ctx *gin.Context) { | ||||||
|  | 	sessionId := ctx.Param("session_id") | ||||||
|  | 	filePath := ctx.Query("path") | ||||||
|  |  | ||||||
|  | 	if filePath == "" { | ||||||
|  | 		ctx.JSON(http.StatusBadRequest, HttpResponse{ | ||||||
|  | 			Code:    http.StatusBadRequest, | ||||||
|  | 			Message: "File path is required", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check permission | ||||||
|  | 	if !c.hasRDPSessionPermission(ctx, sessionId) { | ||||||
|  | 		ctx.JSON(http.StatusForbidden, HttpResponse{ | ||||||
|  | 			Code:    http.StatusForbidden, | ||||||
|  | 			Message: "No permission to access this session", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get session tunnel | ||||||
|  | 	tunnel := c.getRDPSessionTunnel(sessionId) | ||||||
|  | 	if tunnel == nil { | ||||||
|  | 		ctx.JSON(http.StatusNotFound, HttpResponse{ | ||||||
|  | 			Code:    http.StatusNotFound, | ||||||
|  | 			Message: "Session not found or not active", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check if download is allowed | ||||||
|  | 	if !c.isRDPDownloadAllowed(tunnel) { | ||||||
|  | 		ctx.JSON(http.StatusForbidden, HttpResponse{ | ||||||
|  | 			Code:    http.StatusForbidden, | ||||||
|  | 			Message: "File download is disabled for this session", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Request file download through Guacamole protocol | ||||||
|  | 	content, err := c.downloadRDPFile(tunnel, filePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.L().Error("Failed to download file from RDP session", zap.Error(err)) | ||||||
|  | 		ctx.JSON(http.StatusInternalServerError, HttpResponse{ | ||||||
|  | 			Code:    http.StatusInternalServerError, | ||||||
|  | 			Message: "Failed to download file", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Record file operation history | ||||||
|  | 	c.recordRDPFileHistory(ctx, sessionId, "download", filePath, int64(len(content))) | ||||||
|  |  | ||||||
|  | 	// Set response headers | ||||||
|  | 	filename := filepath.Base(filePath) | ||||||
|  | 	ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) | ||||||
|  | 	ctx.Header("Content-Type", "application/octet-stream") | ||||||
|  | 	ctx.Header("Content-Length", strconv.Itoa(len(content))) | ||||||
|  |  | ||||||
|  | 	ctx.Data(http.StatusOK, "application/octet-stream", content) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RDPFileMkdir creates directory in RDP session drive | ||||||
|  | // @Summary Create directory in RDP session | ||||||
|  | // @Description Create directory in RDP session drive | ||||||
|  | // @Tags RDP File | ||||||
|  | // @Accept json | ||||||
|  | // @Produce json | ||||||
|  | // @Param session_id path string true "Session ID" | ||||||
|  | // @Param request body RDPMkdirRequest true "Directory creation request" | ||||||
|  | // @Success 200 {object} HttpResponse | ||||||
|  | // @Router /api/v1/rdp/sessions/{session_id}/files/mkdir [post] | ||||||
|  | func (c *Controller) RDPFileMkdir(ctx *gin.Context) { | ||||||
|  | 	sessionId := ctx.Param("session_id") | ||||||
|  |  | ||||||
|  | 	var req RDPMkdirRequest | ||||||
|  | 	if err := ctx.ShouldBindJSON(&req); err != nil { | ||||||
|  | 		ctx.JSON(http.StatusBadRequest, HttpResponse{ | ||||||
|  | 			Code:    http.StatusBadRequest, | ||||||
|  | 			Message: "Invalid request parameters", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check permission | ||||||
|  | 	if !c.hasRDPSessionPermission(ctx, sessionId) { | ||||||
|  | 		ctx.JSON(http.StatusForbidden, HttpResponse{ | ||||||
|  | 			Code:    http.StatusForbidden, | ||||||
|  | 			Message: "No permission to access this session", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get session tunnel | ||||||
|  | 	tunnel := c.getRDPSessionTunnel(sessionId) | ||||||
|  | 	if tunnel == nil { | ||||||
|  | 		ctx.JSON(http.StatusNotFound, HttpResponse{ | ||||||
|  | 			Code:    http.StatusNotFound, | ||||||
|  | 			Message: "Session not found or not active", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check if upload is allowed (mkdir is considered an upload operation) | ||||||
|  | 	if !c.isRDPUploadAllowed(tunnel) { | ||||||
|  | 		ctx.JSON(http.StatusForbidden, HttpResponse{ | ||||||
|  | 			Code:    http.StatusForbidden, | ||||||
|  | 			Message: "Directory creation is disabled for this session", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Send mkdir request through Guacamole protocol | ||||||
|  | 	err := c.createRDPDirectory(tunnel, req.Path) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.L().Error("Failed to create directory in RDP session", zap.Error(err)) | ||||||
|  | 		ctx.JSON(http.StatusInternalServerError, HttpResponse{ | ||||||
|  | 			Code:    http.StatusInternalServerError, | ||||||
|  | 			Message: "Failed to create directory", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Record file operation history | ||||||
|  | 	c.recordRDPFileHistory(ctx, sessionId, "mkdir", req.Path, 0) | ||||||
|  |  | ||||||
|  | 	ctx.JSON(http.StatusOK, HttpResponse{ | ||||||
|  | 		Code:    0, | ||||||
|  | 		Message: "ok", | ||||||
|  | 		Data: gin.H{ | ||||||
|  | 			"message": "Directory created successfully", | ||||||
|  | 			"path":    req.Path, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RDP Helper methods | ||||||
|  |  | ||||||
|  | func (c *Controller) hasRDPSessionPermission(ctx *gin.Context, sessionId string) bool { | ||||||
|  | 	// TODO: Implement proper session permission check | ||||||
|  | 	// This should verify that the current user has access to the specified session | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Controller) getRDPSessionTunnel(sessionId string) *guacd.Tunnel { | ||||||
|  | 	// TODO: Implement session tunnel retrieval | ||||||
|  | 	// This should get the active Guacamole tunnel for the session | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Controller) isRDPDriveEnabled(tunnel *guacd.Tunnel) bool { | ||||||
|  | 	// Check if RDP drive is enabled in tunnel configuration | ||||||
|  | 	return tunnel.Config.Parameters["enable-drive"] == "true" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Controller) isRDPUploadAllowed(tunnel *guacd.Tunnel) bool { | ||||||
|  | 	return tunnel.Config.Parameters["disable-upload"] != "true" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Controller) isRDPDownloadAllowed(tunnel *guacd.Tunnel) bool { | ||||||
|  | 	return tunnel.Config.Parameters["disable-download"] != "true" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Controller) requestRDPFileList(tunnel *guacd.Tunnel, path string) ([]RDPFileInfo, error) { | ||||||
|  | 	// TODO: Implement Guacamole protocol communication for file listing | ||||||
|  | 	// This would involve sending appropriate Guacamole instructions and parsing responses | ||||||
|  | 	return nil, fmt.Errorf("not implemented: RDP file listing through Guacamole protocol") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Controller) uploadRDPFile(tunnel *guacd.Tunnel, path string, content []byte) error { | ||||||
|  | 	// TODO: Implement Guacamole protocol communication for file upload | ||||||
|  | 	// This would involve sending file-upload instruction with base64 encoded content | ||||||
|  | 	return fmt.Errorf("not implemented: RDP file upload through Guacamole protocol") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Controller) downloadRDPFile(tunnel *guacd.Tunnel, path string) ([]byte, error) { | ||||||
|  | 	// TODO: Implement Guacamole protocol communication for file download | ||||||
|  | 	// This would involve sending file-download instruction and receiving file data | ||||||
|  | 	return nil, fmt.Errorf("not implemented: RDP file download through Guacamole protocol") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Controller) createRDPDirectory(tunnel *guacd.Tunnel, path string) error { | ||||||
|  | 	// TODO: Implement Guacamole protocol communication for directory creation | ||||||
|  | 	return fmt.Errorf("not implemented: RDP directory creation through Guacamole protocol") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Controller) recordRDPFileHistory(ctx *gin.Context, sessionId, operation, path string, size int64) { | ||||||
|  | 	// Record file operation in history | ||||||
|  | 	history := &model.FileHistory{ | ||||||
|  | 		Uid:       0,  // TODO: Get current user ID | ||||||
|  | 		UserName:  "", // TODO: Get current user name | ||||||
|  | 		AssetId:   0,  // TODO: Get asset ID from session | ||||||
|  | 		AccountId: 0,  // TODO: Get account ID from session | ||||||
|  | 		ClientIp:  ctx.ClientIP(), | ||||||
|  | 		Action:    c.getRDPActionCode(operation), | ||||||
|  | 		Dir:       filepath.Dir(path), | ||||||
|  | 		Filename:  filepath.Base(path), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fileService := service.NewFileService(repository.NewFileRepository(dbpkg.DB)) | ||||||
|  | 	if err := fileService.AddFileHistory(ctx, history); err != nil { | ||||||
|  | 		logger.L().Error("Failed to record RDP file history", zap.Error(err)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Controller) getRDPActionCode(operation string) int { | ||||||
|  | 	switch operation { | ||||||
|  | 	case "upload": | ||||||
|  | 		return model.FILE_ACTION_UPLOAD | ||||||
|  | 	case "download": | ||||||
|  | 		return model.FILE_ACTION_DOWNLOAD | ||||||
|  | 	case "mkdir": | ||||||
|  | 		return model.FILE_ACTION_MKDIR | ||||||
|  | 	default: | ||||||
|  | 		return model.FILE_ACTION_LS | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,21 +1,25 @@ | |||||||
| package service | package service | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"archive/zip" | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"io/fs" | 	"io/fs" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/google/uuid" | 	"github.com/google/uuid" | ||||||
| 	"github.com/pkg/sftp" | 	"github.com/pkg/sftp" | ||||||
| 	"golang.org/x/crypto/ssh" |  | ||||||
|  |  | ||||||
| 	"github.com/veops/oneterm/internal/model" | 	"github.com/veops/oneterm/internal/model" | ||||||
| 	"github.com/veops/oneterm/internal/repository" | 	"github.com/veops/oneterm/internal/repository" | ||||||
| 	"github.com/veops/oneterm/internal/tunneling" | 	"github.com/veops/oneterm/internal/tunneling" | ||||||
| 	dbpkg "github.com/veops/oneterm/pkg/db" | 	dbpkg "github.com/veops/oneterm/pkg/db" | ||||||
|  | 	"github.com/veops/oneterm/pkg/logger" | ||||||
|  | 	"go.uber.org/zap" | ||||||
|  | 	"golang.org/x/crypto/ssh" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| @@ -61,10 +65,13 @@ type FileManager struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| type FileInfo struct { | type FileInfo struct { | ||||||
| 	Name  string `json:"name"` | 	Name    string `json:"name"` | ||||||
| 	IsDir bool   `json:"is_dir"` | 	IsDir   bool   `json:"is_dir"` | ||||||
| 	Size  int64  `json:"size"` | 	Size    int64  `json:"size"` | ||||||
| 	Mode  string `json:"mode"` | 	Mode    string `json:"mode"` | ||||||
|  | 	IsLink  bool   `json:"is_link"` | ||||||
|  | 	Target  string `json:"target"` | ||||||
|  | 	ModTime string `json:"mod_time"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetFileManager() *FileManager { | func GetFileManager() *FileManager { | ||||||
| @@ -122,8 +129,16 @@ type IFileService interface { | |||||||
| 	MkdirAll(ctx context.Context, assetId, accountId int, dir string) error | 	MkdirAll(ctx context.Context, assetId, accountId int, dir string) error | ||||||
| 	Create(ctx context.Context, assetId, accountId int, path string) (io.WriteCloser, error) | 	Create(ctx context.Context, assetId, accountId int, path string) (io.WriteCloser, error) | ||||||
| 	Open(ctx context.Context, assetId, accountId int, path string) (io.ReadCloser, error) | 	Open(ctx context.Context, assetId, accountId int, path string) (io.ReadCloser, error) | ||||||
|  | 	Stat(ctx context.Context, assetId, accountId int, path string) (fs.FileInfo, error) | ||||||
|  | 	DownloadMultiple(ctx context.Context, assetId, accountId int, dir string, filenames []string) (io.ReadCloser, string, int64, error) | ||||||
| 	AddFileHistory(ctx context.Context, history *model.FileHistory) error | 	AddFileHistory(ctx context.Context, history *model.FileHistory) error | ||||||
| 	GetFileHistory(ctx context.Context, filters map[string]interface{}) ([]*model.FileHistory, int64, error) | 	GetFileHistory(ctx context.Context, filters map[string]interface{}) ([]*model.FileHistory, int64, error) | ||||||
|  |  | ||||||
|  | 	// RDP file transfer methods | ||||||
|  | 	RDPReadDir(ctx context.Context, sessionId, dir string) ([]fs.FileInfo, error) | ||||||
|  | 	RDPMkdirAll(ctx context.Context, sessionId, dir string) error | ||||||
|  | 	RDPUploadFile(ctx context.Context, sessionId, filename string, content []byte) error | ||||||
|  | 	RDPDownloadFile(ctx context.Context, sessionId, filename string) ([]byte, error) | ||||||
| } | } | ||||||
|  |  | ||||||
| // File service implementation | // File service implementation | ||||||
| @@ -161,6 +176,7 @@ func (s *FileService) Create(ctx context.Context, assetId, accountId int, path s | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return cli.Create(path) | 	return cli.Create(path) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -170,9 +186,159 @@ func (s *FileService) Open(ctx context.Context, assetId, accountId int, path str | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return cli.Open(path) | 	return cli.Open(path) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Stat gets file/directory information | ||||||
|  | func (s *FileService) Stat(ctx context.Context, assetId, accountId int, path string) (fs.FileInfo, error) { | ||||||
|  | 	cli, err := GetFileManager().GetFileClient(assetId, accountId) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return cli.Stat(path) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DownloadMultiple handles downloading single file or multiple files/directories as ZIP | ||||||
|  | func (s *FileService) DownloadMultiple(ctx context.Context, assetId, accountId int, dir string, filenames []string) (io.ReadCloser, string, int64, error) { | ||||||
|  | 	cli, err := GetFileManager().GetFileClient(assetId, accountId) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, "", 0, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Validate and sanitize all filenames for security | ||||||
|  | 	var sanitizedFilenames []string | ||||||
|  | 	for _, filename := range filenames { | ||||||
|  | 		sanitized, err := sanitizeFilename(filename) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, "", 0, fmt.Errorf("invalid filename '%s': %v", filename, err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		sanitizedFilenames = append(sanitizedFilenames, sanitized) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// If only one file, check if it's a regular file first | ||||||
|  | 	if len(sanitizedFilenames) == 1 { | ||||||
|  | 		fullPath := filepath.Join(dir, sanitizedFilenames[0]) | ||||||
|  | 		fileInfo, err := cli.Stat(fullPath) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, "", 0, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// If it's a regular file, return directly | ||||||
|  | 		if !fileInfo.IsDir() { | ||||||
|  | 			reader, err := cli.Open(fullPath) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, "", 0, err | ||||||
|  | 			} | ||||||
|  | 			return reader, sanitizedFilenames[0], fileInfo.Size(), nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Multiple files or contains directory, create ZIP | ||||||
|  | 	return s.createZipArchive(cli, dir, sanitizedFilenames) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // createZipArchive creates a ZIP archive containing the specified files/directories | ||||||
|  | func (s *FileService) createZipArchive(cli *sftp.Client, baseDir string, filenames []string) (io.ReadCloser, string, int64, error) { | ||||||
|  | 	// Generate ZIP filename | ||||||
|  | 	var zipName string | ||||||
|  | 	if len(filenames) == 1 { | ||||||
|  | 		zipName = filenames[0] + ".zip" | ||||||
|  | 	} else { | ||||||
|  | 		zipName = "download.zip" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Use pipe for true streaming without memory buffering | ||||||
|  | 	pipeReader, pipeWriter := io.Pipe() | ||||||
|  |  | ||||||
|  | 	// Create ZIP in a separate goroutine | ||||||
|  | 	go func() { | ||||||
|  | 		defer pipeWriter.Close() | ||||||
|  |  | ||||||
|  | 		zipWriter := zip.NewWriter(pipeWriter) | ||||||
|  | 		defer zipWriter.Close() | ||||||
|  |  | ||||||
|  | 		// Add each file/directory to ZIP | ||||||
|  | 		for _, filename := range filenames { | ||||||
|  | 			fullPath := filepath.Join(baseDir, filename) | ||||||
|  |  | ||||||
|  | 			if err := s.addToZip(cli, zipWriter, baseDir, filename, fullPath); err != nil { | ||||||
|  | 				logger.L().Error("Failed to add file to ZIP", zap.String("path", fullPath), zap.Error(err)) | ||||||
|  | 				pipeWriter.CloseWithError(err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	// Return pipe reader for streaming, size unknown (-1) | ||||||
|  | 	return pipeReader, zipName, -1, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // addToZip recursively adds files/directories to ZIP archive | ||||||
|  | func (s *FileService) addToZip(cli *sftp.Client, zipWriter *zip.Writer, baseDir, relativePath, fullPath string) error { | ||||||
|  | 	fileInfo, err := cli.Stat(fullPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if fileInfo.IsDir() { | ||||||
|  | 		// Add directory | ||||||
|  | 		return s.addDirToZip(cli, zipWriter, fullPath, relativePath) | ||||||
|  | 	} else { | ||||||
|  | 		// Add file | ||||||
|  | 		return s.addFileToZip(cli, zipWriter, fullPath, relativePath) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // addFileToZip adds a single file to ZIP archive | ||||||
|  | func (s *FileService) addFileToZip(cli *sftp.Client, zipWriter *zip.Writer, fullPath, relativePath string) error { | ||||||
|  | 	// Open remote file | ||||||
|  | 	file, err := cli.Open(fullPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer file.Close() | ||||||
|  |  | ||||||
|  | 	// Create file in ZIP | ||||||
|  | 	zipFile, err := zipWriter.Create(relativePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Copy file content | ||||||
|  | 	_, err = io.Copy(zipFile, file) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // addDirToZip recursively adds a directory to ZIP archive | ||||||
|  | func (s *FileService) addDirToZip(cli *sftp.Client, zipWriter *zip.Writer, fullPath, relativePath string) error { | ||||||
|  | 	// Read directory contents | ||||||
|  | 	entries, err := cli.ReadDir(fullPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// If directory is empty, create directory entry | ||||||
|  | 	if len(entries) == 0 { | ||||||
|  | 		_, err := zipWriter.Create(relativePath + "/") | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Recursively add each entry in the directory | ||||||
|  | 	for _, entry := range entries { | ||||||
|  | 		entryFullPath := filepath.Join(fullPath, entry.Name()) | ||||||
|  | 		entryRelativePath := filepath.Join(relativePath, entry.Name()) | ||||||
|  |  | ||||||
|  | 		if err := s.addToZip(cli, zipWriter, fullPath, entryRelativePath, entryFullPath); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // AddFileHistory adds a file history record | // AddFileHistory adds a file history record | ||||||
| func (s *FileService) AddFileHistory(ctx context.Context, history *model.FileHistory) error { | func (s *FileService) AddFileHistory(ctx context.Context, history *model.FileHistory) error { | ||||||
| 	return s.repo.AddFileHistory(ctx, history) | 	return s.repo.AddFileHistory(ctx, history) | ||||||
| @@ -182,3 +348,65 @@ func (s *FileService) AddFileHistory(ctx context.Context, history *model.FileHis | |||||||
| func (s *FileService) GetFileHistory(ctx context.Context, filters map[string]interface{}) ([]*model.FileHistory, int64, error) { | func (s *FileService) GetFileHistory(ctx context.Context, filters map[string]interface{}) ([]*model.FileHistory, int64, error) { | ||||||
| 	return s.repo.GetFileHistory(ctx, filters) | 	return s.repo.GetFileHistory(ctx, filters) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // RDP file transfer methods implementation | ||||||
|  |  | ||||||
|  | // RDPReadDir reads directory contents for RDP session | ||||||
|  | func (s *FileService) RDPReadDir(ctx context.Context, sessionId, dir string) ([]fs.FileInfo, error) { | ||||||
|  | 	// Get session tunnel to access file transfer manager | ||||||
|  | 	tunnel := tunneling.GetTunnelBySessionId(sessionId) | ||||||
|  | 	if tunnel == nil { | ||||||
|  | 		return nil, fmt.Errorf("session not found: %s", sessionId) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// For RDP sessions, we need to check if drive is enabled | ||||||
|  | 	// This would need to be implemented based on the actual session configuration | ||||||
|  | 	// For now, return an error indicating RDP file operations need to be handled differently | ||||||
|  | 	return nil, fmt.Errorf("RDP file operations should be handled through Guacamole protocol") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RDPMkdirAll creates directory for RDP session | ||||||
|  | func (s *FileService) RDPMkdirAll(ctx context.Context, sessionId, dir string) error { | ||||||
|  | 	tunnel := tunneling.GetTunnelBySessionId(sessionId) | ||||||
|  | 	if tunnel == nil { | ||||||
|  | 		return fmt.Errorf("session not found: %s", sessionId) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return fmt.Errorf("RDP file operations should be handled through Guacamole protocol") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RDPUploadFile uploads file for RDP session | ||||||
|  | func (s *FileService) RDPUploadFile(ctx context.Context, sessionId, filename string, content []byte) error { | ||||||
|  | 	tunnel := tunneling.GetTunnelBySessionId(sessionId) | ||||||
|  | 	if tunnel == nil { | ||||||
|  | 		return fmt.Errorf("session not found: %s", sessionId) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return fmt.Errorf("RDP file operations should be handled through Guacamole protocol") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RDPDownloadFile downloads file for RDP session | ||||||
|  | func (s *FileService) RDPDownloadFile(ctx context.Context, sessionId, filename string) ([]byte, error) { | ||||||
|  | 	tunnel := tunneling.GetTunnelBySessionId(sessionId) | ||||||
|  | 	if tunnel == nil { | ||||||
|  | 		return nil, fmt.Errorf("session not found: %s", sessionId) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil, fmt.Errorf("RDP file operations should be handled through Guacamole protocol") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func sanitizeFilename(filename string) (string, error) { | ||||||
|  | 	// Remove any path traversal attempts | ||||||
|  | 	if strings.Contains(filename, "..") || | ||||||
|  | 		strings.Contains(filename, "/") || | ||||||
|  | 		strings.Contains(filename, "\\") { | ||||||
|  | 		return "", fmt.Errorf("invalid filename: path traversal detected") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Remove null bytes and control characters | ||||||
|  | 	if strings.ContainsAny(filename, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f") { | ||||||
|  | 		return "", fmt.Errorf("invalid filename: control characters detected") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return filename, nil | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 pycook
					pycook