mirror of
https://github.com/veops/oneterm.git
synced 2025-10-31 02:46:29 +08:00
466 lines
14 KiB
Go
466 lines
14 KiB
Go
package file
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/veops/oneterm/internal/guacd"
|
|
gsession "github.com/veops/oneterm/internal/session"
|
|
)
|
|
|
|
// RDP file operation functions
|
|
|
|
// NewRDPProgressWriter creates a new RDP progress writer
|
|
func NewRDPProgressWriter(writer io.Writer, transfer *guacd.FileTransfer, transferId string) *RDPProgressWriter {
|
|
return &RDPProgressWriter{
|
|
writer: writer,
|
|
transfer: transfer,
|
|
transferId: transferId,
|
|
written: 0,
|
|
}
|
|
}
|
|
|
|
func (pw *RDPProgressWriter) Write(p []byte) (int, error) {
|
|
n, err := pw.writer.Write(p)
|
|
if err != nil {
|
|
return n, err
|
|
}
|
|
|
|
pw.written += int64(n)
|
|
|
|
// Update unified progress tracking
|
|
UpdateTransferProgress(pw.transferId, 0, pw.written, "transferring")
|
|
|
|
return n, nil
|
|
}
|
|
|
|
// IsRDPDriveEnabled checks if RDP drive is enabled
|
|
func IsRDPDriveEnabled(tunnel *guacd.Tunnel) bool {
|
|
if tunnel == nil || tunnel.Config == nil {
|
|
return false
|
|
}
|
|
driveEnabled := tunnel.Config.Parameters["enable-drive"] == "true"
|
|
return driveEnabled
|
|
}
|
|
|
|
// IsRDPUploadAllowed checks if RDP upload is allowed
|
|
func IsRDPUploadAllowed(tunnel *guacd.Tunnel) bool {
|
|
if tunnel == nil || tunnel.Config == nil {
|
|
return false
|
|
}
|
|
return tunnel.Config.Parameters["disable-upload"] != "true"
|
|
}
|
|
|
|
// IsRDPDownloadAllowed checks if RDP download is allowed
|
|
func IsRDPDownloadAllowed(tunnel *guacd.Tunnel) bool {
|
|
if tunnel == nil || tunnel.Config == nil {
|
|
return false
|
|
}
|
|
return tunnel.Config.Parameters["disable-download"] != "true"
|
|
}
|
|
|
|
// RequestRDPFileList gets file list for RDP session
|
|
func RequestRDPFileList(tunnel *guacd.Tunnel, path string) ([]RDPFileInfo, error) {
|
|
// Implementation placeholder - this would need to be implemented based on Guacamole protocol
|
|
return RequestRDPFileListViaDirect(tunnel, path)
|
|
}
|
|
|
|
func RequestRDPFileListViaDirect(tunnel *guacd.Tunnel, path string) ([]RDPFileInfo, error) {
|
|
// Get session to extract asset ID
|
|
sessionId := tunnel.SessionId
|
|
onlineSession := gsession.GetOnlineSessionById(sessionId)
|
|
if onlineSession == nil {
|
|
return nil, fmt.Errorf("session not found: %s", sessionId)
|
|
}
|
|
|
|
// Get drive path with proper fallback handling
|
|
drivePath, err := DefaultFileService.GetRDPDrivePath(onlineSession.AssetId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get drive path: %w", err)
|
|
}
|
|
|
|
// Validate and construct full filesystem path
|
|
fullPath, err := DefaultFileService.ValidateAndNormalizePath(drivePath, path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid path: %w", err)
|
|
}
|
|
|
|
// Read directory contents
|
|
entries, err := os.ReadDir(fullPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read directory: %w", err)
|
|
}
|
|
|
|
var files []RDPFileInfo
|
|
for _, entry := range entries {
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
continue // Skip entries with errors
|
|
}
|
|
|
|
files = append(files, RDPFileInfo{
|
|
Name: entry.Name(),
|
|
Size: info.Size(),
|
|
IsDir: entry.IsDir(),
|
|
ModTime: info.ModTime().Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
return files, nil
|
|
}
|
|
|
|
// DownloadRDPFile downloads a single file from RDP session
|
|
func DownloadRDPFile(tunnel *guacd.Tunnel, path string) (io.ReadCloser, int64, error) {
|
|
// Get session to extract asset ID
|
|
sessionId := tunnel.SessionId
|
|
onlineSession := gsession.GetOnlineSessionById(sessionId)
|
|
if onlineSession == nil {
|
|
return nil, 0, fmt.Errorf("session not found: %s", sessionId)
|
|
}
|
|
|
|
// Get drive path with proper fallback handling
|
|
drivePath, err := DefaultFileService.GetRDPDrivePath(onlineSession.AssetId)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to get drive path: %w", err)
|
|
}
|
|
|
|
// Validate and construct full filesystem path
|
|
fullPath, err := DefaultFileService.ValidateAndNormalizePath(drivePath, path)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("invalid path: %w", err)
|
|
}
|
|
|
|
// Check if path exists and is a file
|
|
info, err := os.Stat(fullPath)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("file not found: %w", err)
|
|
}
|
|
|
|
if info.IsDir() {
|
|
return nil, 0, fmt.Errorf("path is a directory, not a file")
|
|
}
|
|
|
|
// Open file for streaming (memory-efficient)
|
|
file, err := os.Open(fullPath)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
|
|
return file, info.Size(), nil
|
|
}
|
|
|
|
// DownloadRDPMultiple downloads multiple files from RDP session as ZIP
|
|
func DownloadRDPMultiple(tunnel *guacd.Tunnel, dir string, filenames []string) (io.ReadCloser, string, int64, error) {
|
|
var sanitizedFilenames []string
|
|
for _, filename := range filenames {
|
|
if filename == "" || strings.Contains(filename, "..") || strings.Contains(filename, "/") {
|
|
return nil, "", 0, fmt.Errorf("invalid filename: %s", filename)
|
|
}
|
|
sanitizedFilenames = append(sanitizedFilenames, filename)
|
|
}
|
|
|
|
if len(sanitizedFilenames) == 1 {
|
|
fullPath := filepath.Join(dir, sanitizedFilenames[0])
|
|
|
|
// Check if it's a directory or file
|
|
sessionId := tunnel.SessionId
|
|
onlineSession := gsession.GetOnlineSessionById(sessionId)
|
|
if onlineSession == nil {
|
|
return nil, "", 0, fmt.Errorf("session not found: %s", sessionId)
|
|
}
|
|
|
|
drivePath, err := DefaultFileService.GetRDPDrivePath(onlineSession.AssetId)
|
|
if err != nil {
|
|
return nil, "", 0, fmt.Errorf("failed to get drive path: %w", err)
|
|
}
|
|
|
|
realPath, err := DefaultFileService.ValidateAndNormalizePath(drivePath, fullPath)
|
|
if err != nil {
|
|
return nil, "", 0, fmt.Errorf("invalid path: %w", err)
|
|
}
|
|
|
|
info, err := os.Stat(realPath)
|
|
if err != nil {
|
|
return nil, "", 0, fmt.Errorf("file not found: %w", err)
|
|
}
|
|
|
|
if info.IsDir() {
|
|
// For directory, create a zip with directory contents
|
|
return CreateRDPZip(tunnel, dir, sanitizedFilenames)
|
|
} else {
|
|
// For single file, download directly
|
|
reader, fileSize, err := DownloadRDPFile(tunnel, fullPath)
|
|
if err != nil {
|
|
return nil, "", 0, err
|
|
}
|
|
return reader, sanitizedFilenames[0], fileSize, nil
|
|
}
|
|
}
|
|
|
|
// Multiple files/directories - always create zip
|
|
return CreateRDPZip(tunnel, dir, sanitizedFilenames)
|
|
}
|
|
|
|
// CreateRDPZip creates a ZIP archive of multiple RDP files
|
|
func CreateRDPZip(tunnel *guacd.Tunnel, dir string, filenames []string) (io.ReadCloser, string, int64, error) {
|
|
var buf bytes.Buffer
|
|
zipWriter := zip.NewWriter(&buf)
|
|
|
|
for _, filename := range filenames {
|
|
fullPath := filepath.Join(dir, filename)
|
|
err := AddToRDPZip(tunnel, zipWriter, fullPath, filename)
|
|
if err != nil {
|
|
zipWriter.Close()
|
|
return nil, "", 0, fmt.Errorf("failed to add %s to zip: %w", filename, err)
|
|
}
|
|
}
|
|
|
|
if err := zipWriter.Close(); err != nil {
|
|
return nil, "", 0, fmt.Errorf("failed to close zip: %w", err)
|
|
}
|
|
|
|
downloadFilename := fmt.Sprintf("rdp_files_%s.zip", time.Now().Format("20060102_150405"))
|
|
reader := io.NopCloser(bytes.NewReader(buf.Bytes()))
|
|
return reader, downloadFilename, int64(buf.Len()), nil
|
|
}
|
|
|
|
// AddToRDPZip adds a file or directory to the ZIP archive
|
|
func AddToRDPZip(tunnel *guacd.Tunnel, zipWriter *zip.Writer, fullPath, zipPath string) error {
|
|
// Get session to extract asset ID
|
|
sessionId := tunnel.SessionId
|
|
onlineSession := gsession.GetOnlineSessionById(sessionId)
|
|
if onlineSession == nil {
|
|
return fmt.Errorf("session not found: %s", sessionId)
|
|
}
|
|
|
|
// Get drive path with proper fallback handling
|
|
drivePath, err := DefaultFileService.GetRDPDrivePath(onlineSession.AssetId)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get drive path: %w", err)
|
|
}
|
|
|
|
// Validate and construct full filesystem path
|
|
realPath, err := DefaultFileService.ValidateAndNormalizePath(drivePath, fullPath)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid path: %w", err)
|
|
}
|
|
|
|
// Check if path exists
|
|
info, err := os.Stat(realPath)
|
|
if err != nil {
|
|
return fmt.Errorf("file not found: %w", err)
|
|
}
|
|
|
|
if info.IsDir() {
|
|
// Add directory entries recursively
|
|
entries, err := os.ReadDir(realPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read directory: %w", err)
|
|
}
|
|
|
|
// Create directory entry in zip if not empty
|
|
if len(entries) == 0 {
|
|
// Create empty directory entry
|
|
dirHeader := &zip.FileHeader{
|
|
Name: zipPath + "/",
|
|
Method: zip.Store,
|
|
}
|
|
_, err := zipWriter.CreateHeader(dirHeader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create directory entry: %w", err)
|
|
}
|
|
} else {
|
|
// Add all files in directory
|
|
for _, entry := range entries {
|
|
entryPath := filepath.Join(fullPath, entry.Name())
|
|
entryZipPath := zipPath + "/" + entry.Name()
|
|
err := AddToRDPZip(tunnel, zipWriter, entryPath, entryZipPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Add file to zip
|
|
file, err := os.Open(realPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
writer, err := zipWriter.Create(zipPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create zip entry: %w", err)
|
|
}
|
|
|
|
// Stream file content to zip (memory-efficient)
|
|
if _, err := io.Copy(writer, file); err != nil {
|
|
return fmt.Errorf("failed to write file to zip: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateRDPDirectory creates a directory in RDP session
|
|
func CreateRDPDirectory(tunnel *guacd.Tunnel, path string) error {
|
|
// Get session to extract asset ID
|
|
sessionId := tunnel.SessionId
|
|
onlineSession := gsession.GetOnlineSessionById(sessionId)
|
|
if onlineSession == nil {
|
|
return fmt.Errorf("session not found: %s", sessionId)
|
|
}
|
|
|
|
// Get drive path with proper fallback handling
|
|
drivePath, err := DefaultFileService.GetRDPDrivePath(onlineSession.AssetId)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get drive path: %w", err)
|
|
}
|
|
|
|
// Validate and construct full filesystem path
|
|
fullPath, err := DefaultFileService.ValidateAndNormalizePath(drivePath, path)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid path: %w", err)
|
|
}
|
|
|
|
// Create directory with proper permissions
|
|
if err := os.MkdirAll(fullPath, 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory: %w", err)
|
|
}
|
|
|
|
// Send refresh notification to RDP session
|
|
NotifyRDPDirectoryRefresh(sessionId)
|
|
|
|
return nil
|
|
}
|
|
|
|
// UploadRDPFileStreamWithID uploads file to RDP session with progress tracking
|
|
func UploadRDPFileStreamWithID(tunnel *guacd.Tunnel, transferID, sessionId, path string, reader io.Reader, totalSize int64) error {
|
|
// Get session to extract asset ID
|
|
onlineSession := gsession.GetOnlineSessionById(sessionId)
|
|
if onlineSession == nil {
|
|
return fmt.Errorf("session not found: %s", sessionId)
|
|
}
|
|
|
|
// Get drive path with proper fallback handling
|
|
drivePath, err := DefaultFileService.GetRDPDrivePath(onlineSession.AssetId)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get drive path: %w", err)
|
|
}
|
|
|
|
// Create transfer tracker
|
|
var transfer *guacd.FileTransfer
|
|
if transferID != "" {
|
|
transfer, err = guacd.DefaultFileTransferManager.CreateUploadWithID(transferID, sessionId, filepath.Base(path), drivePath)
|
|
} else {
|
|
transfer, err = guacd.DefaultFileTransferManager.CreateUpload(sessionId, filepath.Base(path), drivePath)
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create transfer tracker: %w", err)
|
|
}
|
|
// Note: Don't remove transfer immediately - let it be cleaned up later so progress can be queried
|
|
|
|
transfer.SetSize(totalSize)
|
|
|
|
// Validate and construct full filesystem path
|
|
fullPath, err := DefaultFileService.ValidateAndNormalizePath(drivePath, path)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid path: %w", err)
|
|
}
|
|
|
|
destDir := filepath.Dir(fullPath)
|
|
if err := os.MkdirAll(destDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create destination directory: %w", err)
|
|
}
|
|
|
|
destFile, err := os.Create(fullPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create file: %w", err)
|
|
}
|
|
defer destFile.Close()
|
|
|
|
progressWriter := NewRDPProgressWriter(destFile, transfer, transferID)
|
|
|
|
buffer := make([]byte, 32*1024)
|
|
written, err := io.CopyBuffer(progressWriter, reader, buffer)
|
|
if err != nil {
|
|
os.Remove(fullPath)
|
|
return fmt.Errorf("failed to write file: %w", err)
|
|
}
|
|
|
|
if totalSize > 0 && written != totalSize {
|
|
os.Remove(fullPath)
|
|
// Mark as failed in unified tracking
|
|
UpdateTransferProgress(transferID, 0, -1, "failed")
|
|
return fmt.Errorf("file size mismatch: expected %d, wrote %d", totalSize, written)
|
|
}
|
|
|
|
// CRITICAL: Explicitly close and sync file before marking as completed
|
|
// This ensures the file is fully written to disk and visible to mounted containers
|
|
if err := destFile.Close(); err != nil {
|
|
return fmt.Errorf("failed to close file: %w", err)
|
|
}
|
|
|
|
// Clear macOS extended attributes that might interfere with Docker volume mounting
|
|
if runtime.GOOS == "darwin" {
|
|
// Clear attributes for the file and parent directory
|
|
exec.Command("xattr", "-c", fullPath).Run()
|
|
exec.Command("xattr", "-c", filepath.Dir(fullPath)).Run()
|
|
}
|
|
|
|
// Mark as completed in unified tracking
|
|
UpdateTransferProgress(transferID, 0, written, "completed")
|
|
|
|
// Send refresh notification to frontend with a slight delay
|
|
// This ensures the file system operations are fully completed
|
|
go func() {
|
|
time.Sleep(500 * time.Millisecond)
|
|
NotifyRDPDirectoryRefresh(sessionId)
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetRDPTransferProgressById gets RDP transfer progress by ID
|
|
func GetRDPTransferProgressById(transferId string) (interface{}, error) {
|
|
progress, err := guacd.DefaultFileTransferManager.GetTransferProgress(transferId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return progress, nil
|
|
}
|
|
|
|
// NotifyRDPDirectoryRefresh sends F5 key to refresh Windows Explorer
|
|
func NotifyRDPDirectoryRefresh(sessionId string) {
|
|
// Get the active session and tunnel
|
|
onlineSession := gsession.GetOnlineSessionById(sessionId)
|
|
if onlineSession == nil {
|
|
return
|
|
}
|
|
|
|
tunnel := onlineSession.GuacdTunnel
|
|
if tunnel == nil {
|
|
return
|
|
}
|
|
|
|
// Send F5 key to refresh Windows Explorer
|
|
// F5 key code: 65474
|
|
f5DownInstruction := guacd.NewInstruction("key", "65474", "1")
|
|
if _, err := tunnel.WriteInstruction(f5DownInstruction); err != nil {
|
|
return
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
f5UpInstruction := guacd.NewInstruction("key", "65474", "0")
|
|
tunnel.WriteInstruction(f5UpInstruction)
|
|
}
|