feat: add SFTP drag-and-drop file handling and upload functionality
- Implemented drag-and-drop support for file uploads in the terminal. - Added a modal for users to choose between uploading files or pasting contents. - Enhanced SFTP browser with a directory picker for selecting upload destinations. - Introduced methods for handling file uploads and reading file contents from the local system. - Added a new API endpoint for uploading raw file data to the server. - Implemented session management improvements to retrieve the current working directory for SSH sessions. - Updated documentation to include an overview and quick start guide for Nexus Terminal.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -1044,6 +1045,22 @@ func (s *Service) SFTPMkdir(sessionID string, remotePath string) error {
|
||||
return s.sessions.SFTPMkdir(sessionID, remotePath)
|
||||
}
|
||||
|
||||
// SFTPStat returns information about a remote path.
|
||||
func (s *Service) SFTPStat(sessionID string, remotePath string) (map[string]interface{}, error) {
|
||||
entry, err := s.sessions.SFTPStat(sessionID, remotePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"name": entry.Name,
|
||||
"path": entry.Path,
|
||||
"size": entry.Size,
|
||||
"isDir": entry.IsDir,
|
||||
"mode": entry.Mode,
|
||||
"modifiedAt": entry.ModifiedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SFTPRemove deletes a remote file or empty directory.
|
||||
func (s *Service) SFTPRemove(sessionID string, remotePath string) error {
|
||||
return s.sessions.SFTPRemove(sessionID, remotePath)
|
||||
@@ -1096,11 +1113,37 @@ func (s *Service) SFTPUpload(sessionID string, remotePath string) error {
|
||||
return s.sessions.SFTPUpload(sessionID, localPath, destPath)
|
||||
}
|
||||
|
||||
// SFTPUploadData uploads raw file data to a remote directory.
|
||||
// Data is expected as base64-encoded bytes from the frontend.
|
||||
func (s *Service) SFTPUploadData(sessionID string, remoteDir string, fileName string, base64Data string) error {
|
||||
if base64Data == "" {
|
||||
return fmt.Errorf("empty upload payload")
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(base64Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode upload data: %w", err)
|
||||
}
|
||||
|
||||
safeName := filepath.Base(fileName)
|
||||
if safeName == "" || safeName == "." || safeName == "/" {
|
||||
return fmt.Errorf("invalid file name")
|
||||
}
|
||||
|
||||
destPath := path.Join(remoteDir, safeName)
|
||||
return s.sessions.SFTPUploadBytes(sessionID, destPath, data)
|
||||
}
|
||||
|
||||
// SFTPPwd returns the current working directory on the remote host.
|
||||
func (s *Service) SFTPPwd(sessionID string) (string, error) {
|
||||
return s.sessions.SFTPPwd(sessionID)
|
||||
}
|
||||
|
||||
// GetSessionCwd returns a best-effort working directory for SSH sessions.
|
||||
func (s *Service) GetSessionCwd(sessionID string) (string, error) {
|
||||
return s.sessions.GetSessionCwd(sessionID)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OpenSSH config import
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -258,6 +258,26 @@ func (m *Manager) ResizeSession(sessionID string, cols, rows int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSessionCwd returns a best-effort working directory for SSH sessions.
|
||||
func (m *Manager) GetSessionCwd(sessionID string) (string, error) {
|
||||
m.mu.RLock()
|
||||
sess, ok := m.sessions[sessionID]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return "", fmt.Errorf("sessions: session %q not found", sessionID)
|
||||
}
|
||||
if sess.sshClient == nil {
|
||||
return "", fmt.Errorf("sessions: session %q is not an SSH connection", sessionID)
|
||||
}
|
||||
|
||||
cwd, err := sess.sshClient.RunCommand("pwd")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(cwd), nil
|
||||
}
|
||||
|
||||
// GetSession returns session info by ID.
|
||||
func (m *Manager) GetSession(sessionID string) (*models.SessionInfo, error) {
|
||||
m.mu.RLock()
|
||||
@@ -1074,6 +1094,23 @@ func (m *Manager) SFTPUpload(sessionID, localPath, remotePath string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// SFTPUploadBytes uploads in-memory data to a remote path.
|
||||
func (m *Manager) SFTPUploadBytes(sessionID, remotePath string, data []byte) error {
|
||||
sc, err := m.getSFTP(sessionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sc.UploadBytes(remotePath, data, func(p sftpclient.TransferProgress) {
|
||||
m.emitEvent("session:sftp:progress", map[string]interface{}{
|
||||
"sessionID": sessionID,
|
||||
"transferred": p.Transferred,
|
||||
"total": p.Total,
|
||||
"path": p.Path,
|
||||
"done": p.Done,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// SFTPPwd returns the current working directory on the remote host.
|
||||
func (m *Manager) SFTPPwd(sessionID string) (string, error) {
|
||||
sc, err := m.getSFTP(sessionID)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
package sftpclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -247,6 +248,25 @@ func (c *Client) Upload(localPath, remotePath string, onProgress func(TransferPr
|
||||
return c.copyWithProgress(local, remote, totalSize, remotePath, onProgress)
|
||||
}
|
||||
|
||||
// UploadBytes writes in-memory data to a remote file path.
|
||||
func (c *Client) UploadBytes(remotePath string, data []byte, onProgress func(TransferProgress)) error {
|
||||
c.mu.Lock()
|
||||
if c.closed || c.sftp == nil {
|
||||
c.mu.Unlock()
|
||||
return fmt.Errorf("sftpclient: client is closed")
|
||||
}
|
||||
remote, err := c.sftp.Create(remotePath)
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return fmt.Errorf("sftpclient: failed to create remote file: %w", err)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
defer remote.Close()
|
||||
|
||||
reader := bytes.NewReader(data)
|
||||
return c.copyWithProgress(reader, remote, int64(len(data)), remotePath, onProgress)
|
||||
}
|
||||
|
||||
// Pwd returns the current working directory on the remote host.
|
||||
func (c *Client) Pwd() (string, error) {
|
||||
c.mu.Lock()
|
||||
|
||||
@@ -261,6 +261,29 @@ func (c *Client) Resize(cols, rows int) error {
|
||||
return session.WindowChange(rows, cols)
|
||||
}
|
||||
|
||||
// RunCommand executes a single command using a new SSH session.
|
||||
func (c *Client) RunCommand(command string) (string, error) {
|
||||
c.mu.Lock()
|
||||
if !c.connected || c.sshClient == nil {
|
||||
c.mu.Unlock()
|
||||
return "", fmt.Errorf("not connected")
|
||||
}
|
||||
client := c.sshClient
|
||||
c.mu.Unlock()
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open SSH session: %w", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
out, err := session.Output(command)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("command failed: %w", err)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
// Close cleanly shuts down the SSH session and connection, cancels the
|
||||
// background context, and invokes the onClose callback.
|
||||
func (c *Client) Close() error {
|
||||
|
||||
Reference in New Issue
Block a user