package api import ( "context" "fmt" "next-terminal/server/common" "next-terminal/server/common/maps" "next-terminal/server/global/session" "next-terminal/server/log" "next-terminal/server/model" "next-terminal/server/repository" "next-terminal/server/utils" "os" "path" "strconv" "strings" "time" "github.com/labstack/echo/v4" "github.com/pkg/sftp" ) type FileSystemApi struct{} func (api FileSystemApi) recordFilesystemLog(ctx context.Context, sessionId, action, fileName string) { sess, err := repository.SessionRepository.FindById(ctx, sessionId) if err != nil { log.Error("Failed to find session for filesystem log", log.NamedError("err", err)) return } filesystemLog := &model.FilesystemLog{ ID: utils.UUID(), AssetId: sess.AssetId, SessionId: sessionId, UserId: sess.Creator, Action: action, FileName: fileName, Created: common.NowJsonTime(), } if err := repository.FilesystemLogRepository.Create(ctx, filesystemLog); err != nil { log.Error("Failed to create filesystem log", log.NamedError("err", err)) } } func (api FileSystemApi) LsEndpoint(c echo.Context) error { sessionId := c.Param("id") dir := c.QueryParam("dir") hiddenFileVisible := c.QueryParam("hiddenFileVisible") == "true" log.Debug("FileSystem Ls: sessionId=" + sessionId + " dir=" + dir) sess := session.GlobalSessionManager.GetById(sessionId) if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil { return Fail(c, -1, "session not found or SSH connection closed") } sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient) if err != nil { log.Debug("FileSystem Ls: failed to create SFTP client: " + err.Error()) return Fail(c, -1, "failed to create SFTP client: "+err.Error()) } defer sftpClient.Close() files, err := sftpClient.ReadDir(dir) if err != nil { log.Debug("FileSystem Ls: failed to read directory: " + err.Error()) return Fail(c, -1, "failed to read directory: "+err.Error()) } var result []maps.Map for _, f := range files { name := f.Name() if !hiddenFileVisible && strings.HasPrefix(name, ".") { continue } result = append(result, maps.Map{ "name": name, "size": f.Size(), "modTime": f.ModTime().UnixMilli(), "path": path.Join(dir, name), "mode": f.Mode().String(), "isDir": f.IsDir(), "isLink": f.Mode()&os.ModeSymlink != 0, }) } return Success(c, result) } func (api FileSystemApi) MkdirEndpoint(c echo.Context) error { sessionId := c.Param("id") dir := c.QueryParam("dir") log.Debug("FileSystem Mkdir: sessionId=" + sessionId + " dir=" + dir) sess := session.GlobalSessionManager.GetById(sessionId) if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil { return Fail(c, -1, "session not found or SSH connection closed") } sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient) if err != nil { return Fail(c, -1, "failed to create SFTP client: "+err.Error()) } defer sftpClient.Close() err = sftpClient.MkdirAll(dir) if err != nil { return Fail(c, -1, "failed to create directory: "+err.Error()) } go api.recordFilesystemLog(context.TODO(), sessionId, "mkdir", dir) return Success(c, nil) } func (api FileSystemApi) TouchEndpoint(c echo.Context) error { sessionId := c.Param("id") filename := c.QueryParam("filename") log.Debug("FileSystem Touch: sessionId=" + sessionId + " filename=" + filename) sess := session.GlobalSessionManager.GetById(sessionId) if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil { return Fail(c, -1, "session not found or SSH connection closed") } sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient) if err != nil { return Fail(c, -1, "failed to create SFTP client: "+err.Error()) } defer sftpClient.Close() f, err := sftpClient.Create(filename) if err != nil { return Fail(c, -1, "failed to create file: "+err.Error()) } f.Close() go api.recordFilesystemLog(context.TODO(), sessionId, "touch", filename) return Success(c, nil) } func (api FileSystemApi) RmEndpoint(c echo.Context) error { sessionId := c.Param("id") filename := c.QueryParam("filename") log.Debug("FileSystem Rm: sessionId=" + sessionId + " filename=" + filename) sess := session.GlobalSessionManager.GetById(sessionId) if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil { return Fail(c, -1, "session not found or SSH connection closed") } sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient) if err != nil { return Fail(c, -1, "failed to create SFTP client: "+err.Error()) } defer sftpClient.Close() f, err := sftpClient.Stat(filename) if err != nil { return Fail(c, -1, "file not found: "+err.Error()) } if f.IsDir() { err = sftpClient.RemoveDirectory(filename) if err != nil { err = api.removeRecursive(sftpClient, filename) if err != nil { return Fail(c, -1, "failed to remove directory: "+err.Error()) } } } else { err = sftpClient.Remove(filename) if err != nil { return Fail(c, -1, "failed to remove file: "+err.Error()) } } go api.recordFilesystemLog(context.TODO(), sessionId, "rm", filename) return Success(c, nil) } func (api FileSystemApi) removeRecursive(sftpClient *sftp.Client, dirPath string) error { files, err := sftpClient.ReadDir(dirPath) if err != nil { return err } for _, f := range files { fullPath := path.Join(dirPath, f.Name()) if f.IsDir() { err = api.removeRecursive(sftpClient, fullPath) if err != nil { return err } } else { err = sftpClient.Remove(fullPath) if err != nil { return err } } } return sftpClient.RemoveDirectory(dirPath) } func (api FileSystemApi) RenameEndpoint(c echo.Context) error { sessionId := c.Param("id") oldName := c.QueryParam("oldName") newName := c.QueryParam("newName") log.Debug("FileSystem Rename: sessionId=" + sessionId + " oldName=" + oldName + " newName=" + newName) sess := session.GlobalSessionManager.GetById(sessionId) if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil { return Fail(c, -1, "session not found or SSH connection closed") } sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient) if err != nil { return Fail(c, -1, "failed to create SFTP client: "+err.Error()) } defer sftpClient.Close() err = sftpClient.Rename(oldName, newName) if err != nil { return Fail(c, -1, "failed to rename: "+err.Error()) } go api.recordFilesystemLog(context.TODO(), sessionId, "rename", oldName+" -> "+newName) return Success(c, nil) } func (api FileSystemApi) EditEndpoint(c echo.Context) error { sessionId := c.Param("id") var req struct { Filename string `json:"filename"` FileContent string `json:"fileContent"` } if err := c.Bind(&req); err != nil { return err } log.Debug("FileSystem Edit: sessionId=" + sessionId + " filename=" + req.Filename) sess := session.GlobalSessionManager.GetById(sessionId) if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil { return Fail(c, -1, "session not found or SSH connection closed") } sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient) if err != nil { return Fail(c, -1, "failed to create SFTP client: "+err.Error()) } defer sftpClient.Close() f, err := sftpClient.OpenFile(req.Filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE) if err != nil { return Fail(c, -1, "failed to open file: "+err.Error()) } defer f.Close() _, err = f.Write([]byte(req.FileContent)) if err != nil { return Fail(c, -1, "failed to write file: "+err.Error()) } go api.recordFilesystemLog(context.TODO(), sessionId, "edit", req.Filename) return Success(c, nil) } func (api FileSystemApi) ReadEndpoint(c echo.Context) error { sessionId := c.Param("id") filename := c.QueryParam("filename") log.Debug("FileSystem Read: sessionId=" + sessionId + " filename=" + filename) sess := session.GlobalSessionManager.GetById(sessionId) if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil { return Fail(c, -1, "session not found or SSH connection closed") } sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient) if err != nil { return Fail(c, -1, "failed to create SFTP client: "+err.Error()) } defer sftpClient.Close() f, err := sftpClient.Open(filename) if err != nil { return Fail(c, -1, "failed to open file: "+err.Error()) } defer f.Close() content, err := readFileContent(f, 10*1024*1024) if err != nil { return Fail(c, -1, "failed to read file: "+err.Error()) } go api.recordFilesystemLog(context.TODO(), sessionId, "read", filename) return Success(c, maps.Map{ "content": content, "path": filename, }) } func readFileContent(f *sftp.File, maxSize int64) (string, error) { stat, err := f.Stat() if err != nil { return "", err } if stat.Size() > maxSize { return "", fmt.Errorf("file too large (max %d bytes)", maxSize) } buf := make([]byte, stat.Size()) _, err = f.Read(buf) if err != nil { return "", err } return string(buf), nil } func (api FileSystemApi) ChmodEndpoint(c echo.Context) error { sessionId := c.Param("id") filename := c.QueryParam("filename") modeStr := c.QueryParam("mode") log.Debug("FileSystem Chmod: sessionId=" + sessionId + " filename=" + filename + " mode=" + modeStr) sess := session.GlobalSessionManager.GetById(sessionId) if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil { return Fail(c, -1, "session not found or SSH connection closed") } mode, err := strconv.ParseUint(modeStr, 10, 32) if err != nil { return Fail(c, -1, "invalid mode: "+err.Error()) } sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient) if err != nil { return Fail(c, -1, "failed to create SFTP client: "+err.Error()) } defer sftpClient.Close() err = sftpClient.Chmod(filename, os.FileMode(mode)) if err != nil { return Fail(c, -1, "failed to chmod: "+err.Error()) } go api.recordFilesystemLog(context.TODO(), sessionId, "chmod", filename+" ("+modeStr+")") return Success(c, nil) } func (api FileSystemApi) UploadProgressEndpoint(c echo.Context) error { sessionId := c.Param("id") id := c.QueryParam("id") log.Debug("FileSystem UploadProgress: sessionId=" + sessionId + " id=" + id) return Success(c, maps.Map{ "total": 0, "written": 0, "percent": 100, "speed": 0, "elapsedTime": 0, "isCompleted": true, }) } func (api FileSystemApi) DownloadEndpoint(c echo.Context) error { sessionId := c.Param("id") filename := c.QueryParam("filename") log.Debug("FileSystem Download: sessionId=" + sessionId + " filename=" + filename) sess := session.GlobalSessionManager.GetById(sessionId) if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil { return Fail(c, -1, "session not found or SSH connection closed") } sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient) if err != nil { return Fail(c, -1, "failed to create SFTP client: "+err.Error()) } defer sftpClient.Close() f, err := sftpClient.Open(filename) if err != nil { return Fail(c, -1, "failed to open file: "+err.Error()) } defer f.Close() stat, err := f.Stat() if err != nil { return Fail(c, -1, "failed to stat file: "+err.Error()) } go api.recordFilesystemLog(context.TODO(), sessionId, "download", filename) c.Response().Header().Set("Content-Disposition", "attachment; filename="+path.Base(filename)) c.Response().Header().Set("Content-Type", "application/octet-stream") c.Response().Header().Set("Content-Length", strconv.FormatInt(stat.Size(), 10)) return c.Stream(200, "application/octet-stream", f) } func (api FileSystemApi) UploadEndpoint(c echo.Context) error { sessionId := c.Param("id") destDir := c.QueryParam("dir") log.Debug("FileSystem Upload: sessionId=" + sessionId + " dir=" + destDir) sess := session.GlobalSessionManager.GetById(sessionId) if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil { return Fail(c, -1, "session not found or SSH connection closed") } file, err := c.FormFile("file") if err != nil { return Fail(c, -1, "failed to get upload file: "+err.Error()) } src, err := file.Open() if err != nil { return Fail(c, -1, "failed to open upload file: "+err.Error()) } defer src.Close() sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient) if err != nil { return Fail(c, -1, "failed to create SFTP client: "+err.Error()) } defer sftpClient.Close() destPath := path.Join(destDir, file.Filename) dst, err := sftpClient.Create(destPath) if err != nil { return Fail(c, -1, "failed to create remote file: "+err.Error()) } defer dst.Close() buf := make([]byte, 32*1024) for { n, err := src.Read(buf) if n > 0 { _, writeErr := dst.Write(buf[:n]) if writeErr != nil { return Fail(c, -1, "failed to write remote file: "+writeErr.Error()) } } if err != nil { break } } go api.recordFilesystemLog(context.TODO(), sessionId, "upload", destPath) return Success(c, maps.Map{ "path": destPath, "size": file.Size, "timestamp": time.Now().UnixMilli(), }) }